Django房屋出租租赁系统实战:状态机、合同与账单设计要点
2026/9/16 12:50:22 网站建设 项目流程

简介:一份基于Python的房屋出租租赁管理系统毕业设计资源,面向计算机相关专业学生及需要快速搭建租赁管理系统的开发者。系统涵盖房源信息管理、租户管理、租赁合同处理、租金计算与支付跟踪等核心模块,采用前后端分离架构,是典型的Web开发参考项目。资源包为ZIP格式,共417个文件、约25.04MB,其中164张JPEG图片多为界面截图,43个Vue文件对应前端组件,31个Python文件承载后端业务逻辑,另有SVG/PNG图标与配置文件,目录结构清晰便于按模块检索。已有159人学习下载,适合在校学生用于毕业设计、课程设计,或开发者借鉴项目整体设计思路。通过该资源可获取完整项目源码、界面素材和配置说明,了解用户登录认证、表单验证、路由设计等实现方式,对掌握Flask/Django开发、数据库设计及前后端交互较有帮助,也便于在此基础上二次扩展。

1. 毕业设计里的房屋出租租赁系统,最容易卡在房间状态对不上

把增删改查写完,一个房屋出租租赁系统只是立住了骨架;真正决定它能否通过答辩、能否接住别人真实业务的是房间状态在时间线上不打架。一间房从空置到预定、签约、退租,再重新上架,任何一个环节跨错了,后续统计出来的空置率和账单都会跟着乱。这类 zip 解压后,大概率是一个 manage.py 开头的 Django 项目,里面通常已经摆了房屋、租客、合同、账单这四张核心表,但表结构本身并不值钱。下面按我拿到这种项目会走的路径,把环境从零搭起来、把状态机和合同生成这些最容易出错的地方讲透。适合正在准备答辩、做课程设计,或者想快速看懂一个 Python 业务系统完整骨架的人。

2. 选型与建模:Django 的骨架和四个核心模型

2.1 为什么这类项目普遍落在 Django 上

房屋租赁系统的数据关系不复杂,但角色边界非常明确。管理员负责录入房间、审核合同,租客要能查自己的账单和缴费记录,这意味着权限不是锦上添花,而是刚性需求。Django 自带人员、权限、会话体系和后台管理,连登录页都不用从头写,对毕业设计来说是框架先替你把一半地基打完了。Flask 虽然更轻,但登录、后台、ORM 都要自己拼,拼出来的脚手架往往不成体系,反而容易在答辩时被追问安全设计。

FastAPI 适合异步 IO 密集的场景,在这里以表单提交和报表查询为主的业务里没有明显收益,引入后还会增加异步 ORM 的心智负担。还有一个常见做法容易被忽略:Django 的迁移工具能在改完模型后一条命令把数据库表同步掉,答辩前几天临时加字段、加索引时,这个能力能省去大量手工维护 SQL 的麻烦。项目如果是按前后端分离做的,还要多维护一套 Vue 构建产物和接口文档,评审现场要跑两个服务,环境成本不低。

2.2 四个核心实体:房屋、租客、合同、账单

这里给出一版最小但够用的 models.py,字段命名尽量贴近业务,不做过度抽象。

from django.db import models class House(models.Model): room_no = models.CharField("房号", max_length=20, unique=True) area = models.DecimalField("面积㎡", max_digits=5, decimal_places=1) monthly_rent = models.DecimalField("月租金", max_digits=8, decimal_places=2) STATUS_CHOICES = ((1, "空置"), (2, "已预定"), (3, "已出租"), (4, "维修中")) status = models.IntegerField("状态", choices=STATUS_CHOICES, default=1) is_active = models.BooleanField("启用", default=True) created_at = models.DateTimeField(auto_now_add=True) class Tenant(models.Model): name = models.CharField("租客姓名", max_length=30) id_card = models.CharField("身份证号", max_length=18, unique=True) phone = models.CharField("联系电话", max_length=11) class LeaseContract(models.Model): contract_no = models.CharField("合同编号", max_length=32, unique=True) house = models.ForeignKey(House, on_delete=models.PROTECT, verbose_name="房屋") tenant = models.ForeignKey(Tenant, on_delete=models.PROTECT, verbose_name="租客") start_date = models.DateField("起租日期") end_date = models.DateField("结束日期") deposit = models.DecimalField("押金", max_digits=8, decimal_places=2) created_at = models.DateTimeField(auto_now_add=True) class RentBill(models.Model): contract = models.ForeignKey(LeaseContract, on_delete=models.CASCADE, verbose_name="合同") bill_month = models.DateField("账期") amount = models.DecimalField("应缴金额", max_digits=8, decimal_places=2) due_date = models.DateField("缴费截止日") paid = models.BooleanField("已缴", default=False) class Meta: constraints = [ models.UniqueConstraint(fields=["contract", "bill_month"], name="uniq_contract_bill_month"), ]

代码里最关键的是两个约束。合同对房屋用on_delete=models.PROTECT而不是 CASCADE,意思是只要存在关联合同就不允许把房屋记录连坐删除,这保护的是业务证据链,防止退租后账单还在却找不到房源信息。账单表的联合唯一约束则从数据库层面兜底,保证同一份合同在同一个账期里不会重复生成两条记录。Hose里的status用 IntegerField 加 choices,后续做统计时可以直接按数字过滤,比存中文字符串更稳。

实体关键字段设计理由
Houseroom_no 唯一房号的业务唯一性由数据库保证,不依赖人工检查
LeaseContract双 PROTECT 外键合同是业务证据,禁止级联删除关联方
RentBill合同+账期联合唯一防止同一月份重复出账
Tenantid_card 唯一同一身份证对应一份长期绑定关系

2.3 先留好三个对状态管理有用的钩子

第一是is_active软删除开关。房源下架时不直接删行,只把is_active改成 False,列表页默认过滤这个字段,这样历史合同、历史账单在关联查询时不会因为外键断裂而查不到。第二是时间戳字段,created_at必须保留,账单逾期计算、合同到期提醒都依赖它。第三是状态字段用 IntegerField 而不是 CharField,然后用 Python 类常量去引用,视图代码里写HouseStatus.RENTED而不是3,可读性和可维护性都会好很多。

这三样东西在写报表时非常有用。空置率统计要区分“维修中”和“空置”,逾期账单需要按账期排序,演示数据造假数据时也要有明确的创建时间线,提前留好钩子后面不用返工。

3. 从 zip 到能跑通:Python 环境、依赖与启动

3.1 解压后先认目录,别急着执行启动命令

拿到 zip 后第一步是解压并看清项目结构,而不是直接找 README 里的命令。

unzip house_system.zip -d house_project cd house_project ls -la

目录里出现manage.py,说明这是一个标准 Django 工程,接下来的所有操作都围绕这个文件展开。requirements.txt是依赖清单,决定你能不能复现作者的运行环境。如果 list 结果里出现venv.venv文件夹,建议先删掉自己重建,因为 Windows 和 Linux 的虚拟环境目录不能互相搬迁,直接复用容易在装包时报出各种路径相关的诡异错误。db.sqlite3很多毕设会一起打包,它能让你先跑起来看到数据,但别依赖里面带的账号,后面自己建一个管理员更省心。

3.2 Python 环境安装与虚拟环境隔离

先确认本机 Python 版本,这一步卡住的人最多。Windows 上打开命令行输入python --version,如果安装时勾选了“Add python.exe to PATH”会直接输出版本;没勾选的话,可以试试py -0列出所有已安装版本,然后py -3.10指定某一个。Linux 系统里通常要输入python3 --version,因为 python 这个命令不一定存在。对这类 Django 项目,常见做法是用 Python 3.8 或 3.10,兼容性最省心。

创建虚拟环境用 Python 自带的 venv 就行,不需要额外装东西。

python -m venv .venv source .venv/bin/activate

Windows 下的激活命令换成.venv\Scripts\activate。激活后命令行前面会出现(.venv)前缀,这时再执行 pip 才不会再污染全局环境。如果本机装了 Anaconda,也可以用 conda 创建:conda create -n house_rent python=3.10 -y && conda activate house_rent。我一般倾向用 venv,毕业设计项目没有引入 conda 的必要,交代码时也少一个环境依赖要求。

提示:用 VSCode 打开项目根目录后,按 Ctrl+Shift+P 打开命令面板,选 “Python: Select Interpreter”,把解释器指向刚创建的 .venv 路径,运行和调试才能识别到项目依赖。

3.3 安装依赖与数据库初始化

依赖安装和数据库初始化是一套组合操作,建议按顺序执行。

pip install -r requirements.txt python manage.py migrate python manage.py createsuperuser python manage.py runserver 127.0.0.1:8000

pip install如果下载慢,可以加镜像源参数:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txtmigrate会把应用里的迁移文件变成真实的数据库表结构,如果报 “No migrations to apply”,多半是自带的 db.sqlite3 已经有旧表,备份后删掉再执行一次即可。createsuperuser按提示输入用户名、邮箱和密码,这个管理员用来登录后台看数据。最后runserver 127.0.0.1:8000启动开发服务器,服务默认监听 8000 端口,浏览器访问http://127.0.0.1:8000就能看到首页。

如果 pip 安装时报 “Please install the missing package” 这类提示,先确认当前是否在激活的虚拟环境里,再检查 requirements.txt 里包名是否完整。在项目里不要 track 这个 zip 文件,而是 track 解压后的工程目录,后续改动才能被识别。

3.4 跑起来后先验证三个入口

入口应该看到的内容异常时的排查方向
/admin后台登录页,能用自己的账号登录检查是否执行了 createsuperuser
/前台首页或房源列表看 urls.py 里根路径是否绑定视图
/house/list 或对应URL房屋列表,包含状态字段看是否完成 migrate,数据表是否存在

打开后台后如果看不到房屋、合同这些模型,多半是 admin.py 里没有注册模型。常见做法是在应用的 admin.py 里写admin.site.register(House)这样一行,模型就会出现在后台导航里。这个步骤虽然简单,但经常被漏掉,导致评审看着后台只有默认的 User 和 Group。

4. 核心业务落地:房间状态机、合同与账单

4.1 状态机:房间状态只能用业务方法改,不能在视图里直接赋值

房屋状态是整套系统的业务锚点。空置、已预定、已出租、维修中这四种状态之间,并不是任意两个都能互相跳转。比如一间房在租约期内,理论上不可能直接变回空置,否则退租日期和合同起止就矛盾了。常见做法是把状态切换收敛到一个显式的方法里,禁止在视图层直接写house.status = 3

class HouseStatus: VACANT = 1 RESERVED = 2 RENTED = 3 MAINTENANCE = 4 ALLOWED_TRANSITIONS = { HouseStatus.VACANT: {HouseStatus.RESERVED, HouseStatus.RENTED, HouseStatus.MAINTENANCE}, HouseStatus.RESERVED: {HouseStatus.VACANT, HouseStatus.RENTED, HouseStatus.MAINTENANCE}, HouseStatus.RENTED: {HouseStatus.MAINTENANCE}, HouseStatus.MAINTENANCE: {HouseStatus.VACANT, HouseStatus.RESERVED}, } def change_house_status(house, new_status, operator=None, remark=""): allowed = ALLOWED_TRANSITIONS.get(house.status, set()) if new_status not in allowed: raise ValueError(f"不允许从状态 {house.status} 切换到 {new_status}") house.status = new_status house.save(update_fields=["status"])

代码的核心是ALLOWED_TRANSITIONS这个字典,它用集合表达了每种状态可以跳转到哪里。change_house_status接收三个参数:house 对象、目标状态、可选的操作用户和备注。先查允许集合,目标状态不在集合里直接抛异常,通过异常信息能快速定位到是哪个业务场景触发的不合法流转。update_fields参数让 save 只更新状态字段,减少无效写库。

当前状态可跳转目标业务含义
空置已预定 / 已出租 / 维修中空房可以预定、直接签约或进入维修
已预定空置 / 已出租 / 维修中预定取消回空置,或签约转在租
已出租维修中租期内维修,不改变合同关系
维修中空置 / 已预定修完再上架,可进入预定

为什么不能直接改字段?因为状态变更往往伴随其他操作。签约要同时生成合同、预定要记录截止时间、退租要触发账单结算,这些动作放在视图里每个地方写一遍,很容易出现“状态改了合同没生成”的情况。把状态机做成方法后,后续如果在里面加一个StatusLog表记录每次切换的操作人和时间,答辩时这就是一个现成的亮点功能。

4.2 合同编号生成与可打印合同实现

合同编号要有业务含义,不能简单用自增 ID。常见格式是“HT + 年月日 + 房号 + 随机数”,既能排序也能防并发重复。

import random from datetime import datetime def generate_contract_no(house_room_no, tenant_id): date_part = datetime.now().strftime("%Y%m%d") return f"HT{date_part}{house_room_no}{random.randint(100, 999)}{tenant_id}"

参数说明:house_room_no直接取房号里的数字部分,tenant_id是租客主键,随机数只做最后一道防撞保险,数据库层还有contract_no的唯一约束兜底。生成合同后,下一步是把它变成能打印的 PDF。这里我强烈建议不要一开始就上 reportlab,中文 PDF 需要在代码里注册字体文件,稍微配置不对就是豆腐块乱码;WeasyPrint 效果虽好,但依赖的系统库较多,在没外网的机器上装起来麻烦。最简单的方案是做一个专门用于打印的 HTML 模板,浏览器按 Ctrl+P 直接另存为 PDF。

<!-- templates/contract_print.html --> <div class="contract"> <h3>房屋租赁合同 <span>{{ contract.contract_no }}</span></h3> <p>出租方:{{ contract.house.owner_name }} 承租方:{{ contract.tenant.name }}</p> <p>租赁期限:{{ contract.start_date }} 至 {{ contract.end_date }}</p> <p>月租金:{{ contract.house.monthly_rent }} 元</p> </div> <style media="print"> @page { margin: 20mm; } .contract { font-size: 14px; line-height: 1.8; } </style>

这套思路的优点是输出干净、自带分页,而且对中文字体完全没要求,浏览器渲染什么就打印什么。需要的只是写一个视图把合同对象塞进模板,返回text/html,用户在当前页面调出打印功能即可。

4.3 按月生成账单与逾期费用计算

账单生成的核心是幂等性,也就是同一合同同一账期只能生成一次。前面的 UniqueConstraint 已经做了数据库兜底,业务代码里用get_or_create配合即可。

from datetime import date def create_rent_bill(contract, bill_year, bill_month, due_day=10): bill_month_date = date(bill_year, bill_month, 1) bill, created = RentBill.objects.get_or_create( contract=contract, bill_month=bill_month_date, defaults={ "amount": contract.house.monthly_rent, "due_date": bill_month_date.replace(day=due_day), }, ) return bill, created def calc_late_fee(bill, daily_rate=0.001, cap_rate=0.1): if bill.paid: return 0 overdue_days = (date.today() - bill.due_date).days if overdue_days <= 0: return 0 fee = bill.amount * daily_rate * overdue_days return min(fee, bill.amount * cap_rate)

get_or_createdefaults参数在记录不存在时生效,已存在时返回旧记录和 False。这样即使脚本被重复执行,也不会产生重复账单。逾期费用按天计算,日费率万分之一?这里参数化后写成了daily_rate=0.001即千分之一,具体数值按题目业务调整。cap_rate设了罚金封顶,避免长期不缴产生离谱金额。

这里特别提醒一个类型转换的坑:due_date是 DateField,计算逾期天数时比较双方都必须是date对象。如果从表单拿到的是"2025-06-10"这样的字符串,要先做一次类型转换再比较,否则会抛出TypeError: can't compare datetime.date to str

4.4 列表查询的 N+1 问题和统计图坐标设置

账单列表页最常见的问题是 N+1 查询:先查 100 条账单,再逐条从外键去查合同和房屋,产生 101 条 SQL。Django ORM 的解决办法很简单。

bill_list = RentBill.objects.filter(paid=False).select_related("contract__house", "contract__tenant")

select_related适用于单值外键关系,contract__house用双下划线跨了两层外键,一条 join 把关联对象全部带出。反过来,如果要查一套合同下的所有账单,那是多值反向关系,该用prefetch_related,两者不要混用。

统计图也是毕设的高频扣分点。如果自己画月租金收入曲线,日期一多横坐标会全部挤在一起。做法是只用 matplotlib 的plt.xticks指定刻度间隔,比如每月只保留一个刻度并旋转 45 度,而不是让默认行为把所有日期全摆上去。这个细节很小,但直接决定图表在答辩投屏上能不能看清楚。

5. 答辩前应该做掉的三件事:依赖清理、演示数据与交付包

5.1 用 pipreqs 清理依赖,而不是 pip freeze

很多人交项目的最后一步是pip freeze > requirements.txt,这会把当前环境里所有无关包全部写进文件,评审在新机器上安装时又慢又容易冲突。常见做法是用 pipreqs 按项目实际 import 生成。

pip install pipreqs pipreqs . --force

--force表示覆盖已有的 requirements.txt。生成后打开文件人工检查一遍,Django、Pillow 这类核心包在不在,版本号是否符合当前代码。如果环境里没有网络,也可以手写一份,只列真实用到的包。

5.2 准备一套有故事情节的演示数据

评审看系统不会从头点菜单,而是带着问题看:有没有逾期账单?空置率怎么算?预定未签的房子长什么样?所以演示数据要有剧情,不能每个房间都长得一样。先想好故事线,再用 Django 的 fixture 机制导出导入。

python manage.py dumpdata house --indent 4 > fixtures/demo.json python manage.py loaddata fixtures/demo.json

dumpdata后面跟应用名,只导业务表,不导 User 和 Session。建议准备一间在租房、一间预定未签、一间维修中、一张已逾期账单,演示时按“正常流程走一遍,再引出异常处理”的顺序讲,比登录后台乱点有说服力得多。

5.3 跑一遍 Django 自带的安全检查

python manage.py check --deploy是 Django 内置的部署安全检查,不装任何第三方扩展就能用。它会输出 CSRF 配置、ALLOWED_HOSTS、DEBUG 开关、SECRET_KEY 相关警告。注意它只做静态检查,不会帮你改配置。本地演示时不要把 DEBUG 改成 False,改静态文件服务会额外引入一堆问题,只要知道检查结果里哪些是真正需要修的即可。

5.4 zip 压缩时的排除规则

保留排除
manage.py、应用目录、templates.venv、pycache、*.pyc
requirements.txt、README、fixtures.git 目录、旧 zip 文件
db.sqlite3(可选,建议排除).idea、.vscode 本地配置
zip -r house_rental_system_final.zip . \ -x ".venv/*" "*__pycache__/*" "*.pyc" ".git/*" "db.sqlite3"

排除.venv是因为压缩包不需要携带完整 Python 环境,评审按 README 里的步骤重建只需要几分钟。注意显式排除了db.sqlite3,如果希望评审解开包就能看到演示数据,把它从排除列表里去掉即可。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询