
简介面向Python课程设计学生的物业管理系统实现方案基于Flask框架构建覆盖用户管理、房屋信息管理、租户管理、费用管理、报修管理等核心模块可帮助初学者快速掌握Web框架、SQLite/MySQL数据库操作及面向对象编程思路。压缩包共285个文件以34个Python源码文件为核心对应项目后端逻辑与数据模型辅以33个HTML页面、31个JS脚本、17个CSS样式及大量PNG/GIF图片素材构成可运行的前端界面整体仅1.92MB轻量且结构清晰便于逐行阅读和二次改造。系统包含用户认证、房屋与租户档案、费用核算与缴费、报修工单追踪等完整流程并体现模型-视图-控制器的分层设计适合作为数据库与Web开发综合实践的参考范例。已有1039人学习下载项目内附有依赖清单和说明文档可降低环境搭建门槛帮助读者从零完成一个可演示的物业管理系统。1. 为什么用 Python 写物业管理系统先说清楚它解决什么一个小区少说上千户物业费、工单、车位、业主信息全挤在微信聊天和 Excel 表里月底一算账就吵架。用 Python 做一套物业管理系统解决的正是这种“数据散、步骤乱、查账难”的日常问题。Python 在这个场景里的优势不是性能而是开发效率和生态Django 自带后台管理、ORM 和认证体系Flask 轻量灵活中小团队或物业公司内部用几周就能跑通一套能用的系统。这篇文章就按最常见的做法走一遍——用 Django 做后端SQLite 起步生产换 MySQL把业主、房产、费用、工单这几张表理清楚再实现缴费、报修、账单生成这些核心动作。适合有一定 Python 基础、想了解业务系统怎么落地的人也适合只拿到一个源码包、想搞明白每个文件在干嘛的读者。2. 物业管理系统的最小数据模型从业主到缴费记录的 Django ORM 设计物业管理系统的核心不是界面而是数据模型。房源、业主、费用、工单这几张表的关系一旦画明白后续所有功能只是在这些关系上做增删改查。这一章用 Django ORM 把最小可用的模型写出来并在表和表之间建立正确的关联。2.1 基础模型设计房产、业主、费用项之间的关系先看 models.py 的完整写法这是整个系统的地基。from django.db import models from django.contrib.auth.models import User from django.utils import timezone class Building(models.Model): 楼栋表 name models.CharField(楼栋名称, max_length50) address models.CharField(地址, max_length200, blankTrue) class Meta: db_table building verbose_name 楼栋 def __str__(self): return self.name class House(models.Model): 房屋表一栋楼有多个房间 building models.ForeignKey(Building, on_deletemodels.CASCADE, verbose_name所属楼栋) room_no models.CharField(房间号, max_length20) area models.DecimalField(建筑面积, max_digits8, decimal_places2) owner models.ForeignKey(Owner, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_namehouses, verbose_name业主) class Meta: db_table house unique_together (building, room_no) def __str__(self): return f{self.building.name}-{self.room_no} class Owner(models.Model): 业主表 name models.CharField(姓名, max_length50) phone models.CharField(手机号, max_length20, uniqueTrue) id_card models.CharField(身份证号, max_length18, blankTrue) user models.OneToOneField(User, on_deletemodels.SET_NULL, nullTrue, blankTrue, verbose_name关联登录账号) class Meta: db_table owner verbose_name 业主 def __str__(self): return f{self.name}({self.phone}) class FeeItem(models.Model): 费用项定义如物业费、水费、停车费 name models.CharField(费用名称, max_length50) unit_price models.DecimalField(单价, max_digits8, decimal_places2) period models.CharField(计费周期, max_length20, help_text例如monthly / quarterly / yearly) is_active models.BooleanField(是否启用, defaultTrue) class Meta: db_table fee_item def __str__(self): return self.name class ChargeRecord(models.Model): 某房屋某费用项的具体账单 house models.ForeignKey(House, on_deletemodels.CASCADE, verbose_name房屋) fee_item models.ForeignKey(FeeItem, on_deletemodels.PROTECT, verbose_name费用项) amount models.DecimalField(应收金额, max_digits10, decimal_places2) due_date models.DateField(缴费截止日) paid_at models.DateTimeField(缴费时间, nullTrue, blankTrue) status models.CharField(状态, max_length10, defaultunpaid, choices((unpaid, 未缴), (paid, 已缴), (overdue, 逾期), (cancelled, 作废))) class Meta: db_table charge_record ordering [-due_date]模型设计的逻辑说明Building 和 House 是一对多House 关联 Owner 用的是可空外键这样卖房后可以解绑再重绑。ChargeRecord 是“属性数据”一条记录就是一份账单通过 house 和 fee_item 两个外键定位到具体房屋和费用类型。最容易被忽略的是unique_together它保证同一栋楼里不会出现重复的房间号比在代码里做检查可靠得多。参数说明on_deletemodels.PROTECT用于 FeeItem 外键意思是费用项一旦被账单引用就不允许直接删除避免历史账单悬空。SET_NULL用于业主外键允许房子暂时没有业主。DecimalField而不是FloatField存金额这是硬性要求浮点误差在钱上不可接受。2.2 数据迁移与初始数据把 Django 项目跑起来数据模型写完需要生成并执行迁移Django 会根据模型自动创建对应的数据库表。python manage.py makemigrations property python manage.py migrate python manage.py createsuperusermakemigrations会根据模型变化生成迁移文件目录在property/migrations/下。migrate把迁移应用到数据库Django 会自动创建 auth_user、django_session 等内置表。createsuperuser创建管理员账号进入 Django admin 后台用。进入后台还需要把模型的 admin 注册配置好这样运营人员可以直接在后台维护楼栋、房产、业主和费用项。# admin.py from django.contrib import admin from .models import Building, House, Owner, FeeItem, ChargeRecord class HouseInline(admin.TabularInline): model House extra 0 admin.register(Building) class BuildingAdmin(admin.ModelAdmin): inlines [HouseInline] # 非必须但推荐设置 list_display提示本地开发用 SQLite不需要额外安装数据库服务。生产环境把 settings.py 里的 DATABASES 改成 MySQL重新 migrate 即可模型代码不用改。3. 跑通核心流程物业费收缴与工单闭环的 Python 实现数据模型只是地基真正让系统有价值的是业务流程。物业管理里有两条核心链路一条是“账单生成 → 确认缴费 → 登记入账”另一条是“业主报修 → 工单派发 → 处理回填 → 业主确认”。两条链路你都用 Python 代码驱动下面分别展开。3.1 物业费账单生成按月结算与滞纳金计算物业费不是一笔笔手动录的而是按周期批量生成。常见做法是每月 1 日跑一次脚本为所有“未缴清且未生成本月账单”的房源生成账单。def generate_monthly_charge(year, month): 为所有未注销的房源生成指定月份的物业费账单 from .models import House, FeeItem, ChargeRecord from datetime import date # 找出启用的物业费计费项一般建议一个小区只有一个“物业费”费用项 fee_item FeeItem.objects.filter(name物业费, is_activeTrue).first() if not fee_item: return houses House.objects.select_related(building).all() records [] for house in houses: # 跳过已经生成过该月账单的房源避免重复生成 exists ChargeRecord.objects.filter( househouse, fee_itemfee_item, due_date__yearyear, due_date__monthmonth, ).exists() if exists: continue amount house.area * fee_item.unit_price # 计算缴费截止日一般设为月底最后一天 due_date date(year, month, 28) records.append(ChargeRecord( househouse, fee_itemfee_item, amountamount.quantize(Decimal(0.01)), due_datedue_date, statusunpaid, )) ChargeRecord.objects.bulk_create(records, batch_size500) return len(records)这里的关键是幂等性判断exists()检查重复保证脚本跑两次不会产生两份账单。bulk_create把上千条记录一次性写入数据库比循环 save 快几十倍。滞纳金计算的常见口径是“逾期不超过 30 天按每日万分之五超过 30 天按千分之一”这只是一种约定具体以物业合同为准def calculate_penalty(charge_record, penalty_rulenormal): 计算滞纳金普通规则日利率0.05%封顶不超过本金 from datetime import date if charge_record.paid_at: return Decimal(0) days (date.today() - charge_record.due_date).days if days 0: return Decimal(0) rate Decimal(0.0005) penalty charge_record.amount * rate * days # 封顶100%避免滞纳金超过本金 if penalty charge_record.amount: penalty charge_record.amount return penalty.quantize(Decimal(0.01))逻辑说明计算从due_date次日到今天的差额再按日利率乘。所有金额都用Decimal运算避免class float带来精度问题。封顶逻辑是业务约束不是数学约束抄代码时留意你所在机构的规则。3.2 报修工单状态机前后台联动不丢单工单系统最怕“业主报修了但没人跟进”技术上就是状态流转失控。用 Python 表达式定义状态迁移。# workorder/models.py class WorkOrder(models.Model): STATUS_CHOICES [ (pending, 待分配), (assigned, 已派工), (processing, 处理中), (completed, 已完工), (confirmed, 业主已确认), (cancelled, 已取消), ] house models.ForeignKey(House, on_deletemodels.CASCADE, verbose_name房屋) desc models.TextField(问题描述) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) assignee models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue) # 状态变更合法性检查 ALLOWED_TRANSITIONS { pending: [assigned, cancelled], assigned: [processing, cancelled], processing: [completed], completed: [confirmed], } def transition(self, to_status): if to_status not in self.ALLOWED_TRANSITIONS.get(self.status, []): raise ValueError(f状态不可从{self.status}变更为{to_status}) self.status to_status self.save(update_fields[status])参数说明ALLOWED_TRANSITIONS是一个字典key 是当前状态value 是允许跳转的目标状态集合。transition方法在修改状态前先校验合法性非法跳转直接抛异常。这种写法让状态流转的逻辑集中在一处不散落在各个视图里。提示状态机规则一定要写入服务层或模型方法不要在每个 view 函数里写 if else否则改一个规则要搜全项目。4. 这一步最容易踩坑CSRF、时区和事务的“物业管理式”问题业务代码写完了但真正上线时最先出问题的往往不是业务逻辑而是框架层面的细节。这一章专门挑三个高频坑来讲每一个都可能让你在调试上白耗半天。4.1 CSRF 校验失败POST 请求被 Django 拦截Django 默认启用 CSRF 中间件所有 POST 请求都要求携带 csrfmiddlewaretoken。页面模板里要用{% csrf_token %}AJAX 请求则要额外处理。// 用 AJAX 提交缴费确认需要先从 cookie 读取 csrftoken function getCookie(name) { let cookieValue null; if (document.cookie document.cookie ! ) { const cookies document.cookie.split(;); for (let i 0; i cookies.length; i) { const cookie cookies[i].trim(); if (cookie.substring(0, name.length 1) (name )) { cookieValue decodeURIComponent(cookie.substring(name.length 1)); break; } } } return cookieValue; } const csrftoken getCookie(csrftoken); fetch(/api/pay/, { method: POST, headers: { Content-Type: application/json, X-CSRFToken: csrftoken, }, body: JSON.stringify({ charge_id: 1024 }), });很多初学者把{% csrf_token %}写在 HTML 页面里但如果用了 Vue 或 React 做前端模板语法根本不执行此时就需要从 cookie 手动读取。Django 默认会在响应中设置csrftokencookie上面的getCookie函数就是标准做法。排查方法如果 AJAX 请求返回 403第一步看浏览器的 Application 面板有没有 csrftoken 这个 cookie。没有的话确认你在视图中使用了render而不是HttpResponse直接拼接 HTML因为render才会帮你注入 CSRF cookie。4.2 时区设置缴费时间记录差 8 小时的隐患物业系统有一个特殊场景业主在手机上缴费财务希望看到的是北京时间而不是 UTC。Django 默认USE_TZ True如果 settings.py 里的TIME_ZONE没改保存到数据库的时间会是 UTC第二天对账差 8 小时。# settings.py USE_TZ True TIME_ZONE Asia/Shanghai设置完成后DateTimeField存入数据库时仍按 UTC 存储但在模板渲染和表单输入时会按TIME_ZONE转换。如果你在代码里用了datetime.now()而不带 tzinfoDjango 会抛出RuntimeWarning甚至直接报错。应使用django.utils.timezone.now()。from django.utils import timezone paid_at timezone.now() # 而不是 from datetime import datetime; datetime.now()一个容易忽略的点报表查询往往要按“今天”过滤数据。如果你写filter(paid_at__datedate.today())Django 会用本地时区转换后再查询这个行为是正确的。但如果你已经在代码里手动把字符串拼进 SQL就很容易出错。4.3 事务回滚批量催退费不要拆成半个请求涉及钱的接口必须保证原子性。比如“批量退费”财务选择 100 条记录点击退费系统执行“把 ChargeRecord.status 改为 cancelled同时往退款表插入一条记录”。如果循环到第 50 条时数据库连接断开前面 49 条已经改了第 50 条之后没有处理账就对不上。from django.db import transaction transaction.atomic def batch_refund(charge_ids, operator): 批量作废账单并登记退款记录 for charge_id in charge_ids: record ChargeRecord.objects.select_for_update().get(idcharge_id) if record.status ! paid: raise ValueError(f账单{charge_id}状态不正确不可退款) record.status cancelled record.save(update_fields[status]) RefundRecord.objects.create( chargerecord, amountrecord.amount, operatoroperator, )transaction.atomic保证函数内所有数据库操作要么全部成功要么全部回滚。select_for_update()对行加锁防止两个管理员同时操作同一笔账单。这两个组合在涉钱操作里是标配。5. 一页报表脚本用 Django 的 aggregate 把物业费收缴率算明白最后做一个实用锦囊不写任何前端页面用 Django shell 或一个脚本文件直接输出物业费收缴率报表。这套逻辑可以放进 cron 每天自动跑也可以临时手动执行看经营状况。5.1 月度收缴率按费用项聚合统计收缴率的定义没有唯一标准物业公司常说的口径是“已收到的钱 ÷ 应收到的钱”也有按“已缴的账单数 ÷ 应缴的账单数”算的。下面是按金额口径的实现。from django.db.models import Sum, Q from datetime import date from property.models import ChargeRecord def monthly_collection_report(year, month): 输出指定月份的物业费收缴汇总 month_start date(year, month, 1) if month 12: month_end date(year 1, 1, 1) else: month_end date(year, month 1, 1) filter_kwargs dict(due_date__gtemonth_start, due_date__ltmonth_end) total ( ChargeRecord.objects.filter(**filter_kwargs) .exclude(statuscancelled) .aggregate(total_amountSum(amount)) ) paid ( ChargeRecord.objects.filter(**filter_kwargs, statuspaid) .aggregate(paid_amountSum(paid_amount)) ) return { total_amount: total[total_amount] or 0, paid_amount: paid[paid_amount] or 0, rate: paid[paid_amount] / total[total_amount] if total else 0, }这里用了两个aggregate查询返回字典而不用手动遍历。第一个查询用exclude(statuscancelled)排除作废记录第二个查询只统计已缴金额。注意Sum可能返回 None需要or 0兜底。5.2 欠费名单找出逾期超过 90 天的房屋催收是物业管理的核心痛点。下面这个查询会列出欠费金额最高的小区房源按金额降序排列可以配合发送短信或打印催缴单。overdue_records ( ChargeRecord.objects .filter(statusunpaid, due_date__ltdate.today() - timedelta(days90)) .annotate(customer_nameF(house__owner__name)) .annotate(phoneF(house__owner__phone)) .annotate(roomF(house__room_no)) .select_related(house__owner, fee_item) .order_by(-amount) )这里用annotate把关联表的字段拉到同一行用select_related避免 N1 查询。F表达式用来在 Python 代码中引用数据库字段。用一条命令跑这个脚本并输出格式化的结果可以把以下代码放入report.py再执行from datetime import date, timedelta from django.db.models import F def print_overdue_summary(): qs ( ChargeRecord.objects .filter(statusunpaid, due_date__ltdate.today() - timedelta(days90)) .annotate(roomF(house__room_no)) .annotate(owner_nameF(house__owner__name)) .annotate(owner_phoneF(house__owner__phone)) .order_by(-amount)[:20] ) print(f{房间:20}{业主:12}{电话:15}{欠费金额:10}) for item in qs: print(f{item.room:20}{item.owner_name:12}{item.owner_phone:15}{item.amount:10})运行方式python manage.py shell -c from property.report import print_overdue_summary; print_overdue_summary()这行命令适合放到 cron 里每天上午 9 点输出一份欠费名单存成文本或邮件发送都行。所有运算都在数据库层面完成即使有 10 万条 ChargeRecord 也不会有性能问题。这段脚本的启发是有很多日常报表可以用 Django ORM 直接输出不一定要写 HTML 页面和图表工具一个方法加上一个视图就够用了。本文还有配套的精品资源点击获取