
手头接了个二手房交易系统的活儿技术栈定的是 Python 和 Django。项目从头梳理、建模、写业务逻辑到部署上线前后折腾了两个多月。中间踩了不少坑也总结出一些跟书籍教程不太一样的东西。这篇把整个开发过程的关键环节捋一遍供准备做类似 Web 项目的朋友参考。1. 项目整体设计与需求拆解1.1 业务痛点与系统目标二手房交易系统表面看是个信息发布平台实际上核心要解决的是三方角色之间的信息匹配与流程管理问题买方要找房源、卖方要挂房源、中介要管理跟进。如果搞不清楚这个基本盘系统很容易做成一个花哨但不好用的展示站。我当时梳理出的核心目标有三条第一房源信息能够结构化录入和高效检索不是贴一段文字完事第二交易流程关键节点预约看房、意向登记、合同签署状态要有记录能查得到第三后台要能支撑运营人员做房源审核和数据分析权限要清晰。这三条听着简单落地时直接影响数据模型设计和功能模块划分。很多团队一开始就纠结界面好不好看反倒忽略了底层结构后期改起来非常痛苦。1.2 功能模块划分与优先级我按业务价值把功能切成几个模块优先级从高到低排列房源管理发布、编辑、上下架、审核这套是绝对的核心用户系统注册登录、身份选择个人/中介、个人中心搜索筛选按区域、价格区间、户型、面积等条件组合查询预约看房买家发起预约房主/中介确认双方在系统里留痕后台管理房源审核、用户管理、数据统计交易跟进合同状态记录、过户进度节点这块最容易被忽视。MVP阶段砍掉了在线签约、资金托管这些重流程功能只保留状态记录。原因是这些功能涉及线下法律流程线上系统能做的是记录和提醒不可能替代实际交易。这个口子必须先跟需求方对齐不然后期会没完没了地加需求。1.3 技术选型的三个落点选 Django 不是因为它最潮而是因为在这个场景下它刚好匹配三个硬性要求。第一个落点是 ORM 能力。房源筛选条件多查询组合复杂Django ORM 的 Q 对象和链式过滤在写动态查询时非常顺手能省掉大量手拼 SQL 的麻烦。第二个落点是自带 Admin。后台管理这个需求几乎是所有管理类系统的标配Django Admin 稍微定制一下就是个能用的运营后台比从头用 Vue 写一套快得多。第三个落点是生态成熟。用户认证、权限控制、表单处理、分页、中间件Django 都有现成方案踩坑的人多意味着解决方案也多。Python 侧的语言优势不用多说写业务逻辑开发效率高配合 Django 的 MTV 架构接口开发节奏很快。至于前段页面我没有用前后端分离而是采用 Django 模板 Bootstrap 的组合一人搞定前后端开发成本最低。如果你打算用 Django 做纯 API配 Vue 或 React架构上也成立只是工作量会多出一个量级。2. 环境搭建与数据模型设计2.1 Python 与 Django 版本组合当前稳定组合是 Python 3.10 配 Django 4.2 LTS。Django 4.2 是长期支持版本官方维护周期到 2026 年用在正式项目上稳当。Python 版本不需要追最新3.10 或者 3.11 都可以。3.12 出来之后我特意测过一些第三方库的兼容性部分库还有小问题项目不着急上。建议创建虚拟环境避免污染全局环境# 创建虚拟环境 python -m venv venv # 激活Windows venv\Scripts\activate # 激活Linux/macOS source venv/bin/activate # 安装 Django pip install django4.2.* pip install mysqlclient # 如果用 MySQL注意MySQL 的驱动安装是个小坑Windows 上直接pip install mysqlclient很可能会报错。推荐直接装pymysql然后在项目的__init__.py里加两句兼容代码简单省事。2.2 项目结构与关键配置我创建项目时没有把 apps 全放一个目录里而是按功能拆开django-admin startproject secondhand_home cd secondhand_home python manage.py startapp users python manage.py startapp houses python manage.py startapp orders python manage.py startapp operations这样拆的好处是业务边界清晰。users 管认证和用户资料houses 管家源orders 管预约和交易流程operations 管后台审核和统计后面做权限控制时顺着这个边界走就对了。配置里有个需要注意的点是AUTH_USER_MODEL。如果系统里有买家和中介两种身份建议从一开始就自定义用户模型不要用 Django 默认的 User。我用的是扩展字段的方式# users/models.py from django.contrib.auth.models import AbstractUser class User(AbstractUser): USER_TYPE_CHOICES ( (buyer, 买家), (agent, 中介), (owner, 房主), ) user_type models.CharField(max_length10, choicesUSER_TYPE_CHOICES, defaultbuyer) phone models.CharField(max_length20, blankTrue) avatar models.ImageField(upload_toavatars/, blankTrue)有了 user_type 之后后续所有业务逻辑判断用户身份就很简单了。这个字段设计建议从第一版就加上后补迁移是很烦的事情。2.3 房源数据模型设计细节房源表是整个系统的中枢字段设计直接影响后面所有功能。我列一下核心表结构# houses/models.py class House(models.Model): title models.CharField(max_length200) house_type models.CharField(max_length20) # 户型比如 3室2厅 area models.DecimalField(max_digits8, decimal_places2) # 面积平米 total_price models.DecimalField(max_digits12, decimal_places2) # 总价万 unit_price models.DecimalField(max_digits10, decimal_places2) # 单价元/平米 district models.CharField(max_length50) # 区域 address models.CharField(max_length255) floor_info models.CharField(max_length50) # 楼层信息 orientation models.CharField(max_length20) # 朝向 decoration models.CharField(max_length20) # 装修情况 description models.TextField(blankTrue) cover_image models.ImageField(upload_tohouses/) status models.CharField( max_length20, choices( (pending, 待审核), (active, 已上架), (inactive, 已下架), (sold, 已成交), ), defaultpending ) owner models.ForeignKey(User, on_deletemodels.CASCADE, related_nameowned_houses) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: indexes [ models.Index(fields[district, status]), models.Index(fields[total_price]), ]几个设计取舍值得说面积和价格都用DecimalField绝不用 FloatField。浮点精度问题在涉及钱的系统里不可接受status字段是从 待审核 到 已上架 到 已成交 的状态机。每次状态变更都建议用 Django 的信号或者 Service 层方法处理不要到处直接改索引设计优先考虑查询条件。distrcit status 的组合索引覆盖了最常用的检索场景。2.4 预约看房与交易记录表预约和交易流程单独建表不塞进房源表里。预约表的逻辑是某个买家对某个房源的一次看房意向# orders/models.py class Appointment(models.Model): house models.ForeignKey(House, on_deletemodels.CASCADE, related_nameappointments) buyer models.ForeignKey(User, on_deletemodels.CASCADE, related_nameappointments) appointed_time models.DateTimeField() status models.CharField( max_length20, choices( (pending, 待确认), (confirmed, 已确认), (completed, 已完成), (cancelled, 已取消), ), defaultpending ) remark models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue)交易流程记录我做了个简单的历史表核心是追踪房源从有买家意向到最终成交的关键节点。这个表在早期版本容易被忽略但后期做数据统计和运营复盘的时候价值很大。3. 核心功能模块开发与实现3.1 用户认证与权限控制Django 自带的认证体系很完善注册、登录、登出都有现成方案。我在上面叠了两件事第一件事是注册时区分用户身份。表单里有一个我是买家/中介/房主的选择对应到user_type字段。这个选择不可随意修改避免有人绕过身份限制。第二件事是权限校验。我封装了个装饰器专门校验用户类型# users/decorators.py from django.core.exceptions import PermissionDenied def user_type_required(*allowed_types): def decorator(view_func): def _wrapped_view(request, *args, **kwargs): if request.user.is_authenticated and request.user.user_type in allowed_types: return view_func(request, *args, **kwargs) raise PermissionDenied return _wrapped_view return decorator用的时候直接user_type_required(owner, agent) def house_create(request): # 只有房主和中介能发布房源 ...权限之外密码安全直接用 Django 默认的 PBKDF2 算法线上开 HTTPS 就不用担心。3.2 房源发布与图片处理房源发布是整个系统里用户操作最多的表单字段十几个图片还可能有多张。Django 的 ModelForm 天然适合这种场景字段声明和模型一一对应验证逻辑也不用手写。图片处理有个非常实用的点就是用 Pillow 对上传图片做压缩和尺寸限制。不处理的话用户拿手机拍一张十来兆的照片直接传上来服务器磁盘压力大页面加载也卡。我在表单里做了校验# houses/forms.py from PIL import Image def clean_cover_image(self): image self.cleaned_data.get(cover_image) if image: if image.size 5 * 1024 * 1024: raise forms.ValidationError(图片大小不能超过5MB) img Image.open(image) if img.width 1200: # 等比压缩到宽度1200 ratio 1200 / img.width new_height int(img.height * ratio) img img.resize((1200, new_height), Image.LANCZOS) image.file img.tobytes() # 需要处理成 BytesIO return image多图上传我用的是django-mptt思路配合简单的图集表或者直接用django-multiupload这类插件也行。实际开发里不必过度设计一个HouseImage外键表就够用class HouseImage(models.Model): house models.ForeignKey(House, on_deletemodels.CASCADE, related_nameimages) image models.ImageField(upload_tohouses/) is_cover models.BooleanField(defaultFalse)3.3 搜索筛选功能的动态查询搜索是二手房系统的重头戏用户一上来就按区域、价格、户型、面积筛。Django ORM 写这种动态查询需要把条件拼起来核心是避免空条件影响结果。方法是用 Q 对象或字典参数组合。我最开始直接用 if 判断拼 filter代码越写越长、越来越乱。后来换成用字典收集所有条件再一次性传入清爽很多# houses/views.py from django.db.models import Q def house_search(request): queryset House.objects.filter(statusactive) district request.GET.get(district) min_price request.GET.get(min_price) max_price request.GET.get(max_price) min_area request.GET.get(min_area) house_type request.GET.get(house_type) orientation request.GET.get(orientation) if district: queryset queryset.filter(districtdistrict) if min_price: queryset queryset.filter(total_price__gtemin_price) if max_price: queryset queryset.filter(total_price__ltemax_price) if min_area: queryset queryset.filter(area__gtemin_area) if house_type: queryset queryset.filter(house_type__icontainshouse_type) if orientation: queryset queryset.filter(orientationorientation) # 排序 sort request.GET.get(sort, latest) if sort price_asc: queryset queryset.order_by(total_price) elif sort price_desc: queryset queryset.order_by(-total_price) else: queryset queryset.order_by(-created_at) paginator Paginator(queryset, 12) # 每页12条 page_number request.GET.get(page) page_obj paginator.get_page(page_number) ...这段代码逻辑看着简单但有几个细节要注意total_price__gtemin_price这种写法能处理价格区间查询Django 会把gte翻译成语义很清楚户型搜索用icontains而不是exact。原因很现实用户输入三室还是3室并不一致模糊匹配能兼容更多输入习惯价格排序会增加数据库的排序开销房源量到万级以后建议在total_price字段上加普通索引。3.4 分页与缓存优化列表页数据量上去之后分页是刚需。Django 自带的Paginator用起来简单但短板是每次翻页都要重新查一遍数据库对数据库压力不小。我当时在房源列表页加了页面级缓存from django.core.cache import cache def house_search(request): cache_key fhouse_list_{request.GET.urlencode()} result cache.get(cache_key) if result is None: # 上面一整套查询逻辑 result { page_obj: page_obj, filters: filters, } cache.set(cache_key, result, 60 * 5) # 缓存5分钟 ...这里加缓存有个坑要提醒页面必须带分页参数不然不同页面的 key 是一样的会出现翻页永远第一页的 bug。我实际开发时把page参数也拼到了 urlencode 里任何筛选条件变化都会生成新的缓存 key。如果数据量更大比如房源过十万优先考虑加 Elasticsearch 做全文检索。Django 的django-haystack中间层可以接 ES或者直接用drf-haystack。但说实话中小型项目的房源量用 MySQL 索引加 Redis 缓存完全扛得住不必过早引入重武器运维成本不低。3.5 后台管理与 Django Admin 定制Django Admin 是这套系统效率最高的部分。我在 Admin 中注册房源、用户、预约等模型后运营人员就能直接操作完全不用额外开发。核心的定制点是列表页展示哪些字段设置list_display右侧筛选器用list_filter按状态和区域筛选顶部搜索框用search_fields支持多字段模糊搜富文本编辑的话重写formfield_for_dbfield挂 CKEditor或者用现成的django-ckeditor包。# operations/admin.py admin.register(House) class HouseAdmin(admin.ModelAdmin): list_display (title, district, total_price, status, owner, created_at) list_filter (status, district, house_type) search_fields (title, address, owner__username) actions [make_active, make_inactive]这里有个容易被忽略的点Admin 默认是无法直接关联操作自定义权限的。如果你想限定某个运营人员只能看不能改要注册Group和Permission通过 Django 自带的权限系统控制。我在项目里建了审核员和管理员两个 Group审核员只有view_house和change_house_status权限避免误操作。3.6 预约流程的状态流转预约看房这个功能看着简单实际上处理好状态流转要花心思。我在 Appointment 模型上定义了一个update_status方法所有状态变更都通过它走# orders/models.py class Appointment(models.Model): ... def update_status(self, new_status, user): allowed_transitions { pending: [confirmed, cancelled], confirmed: [completed, cancelled], completed: [], cancelled: [], } if new_status not in allowed_transitions.get(self.status, []): raise ValueError(f非法状态转换: {self.status} - {new_status}) if new_status in (confirmed, completed) and user.user_type ! agent: raise PermissionDenied(只有中介可以确认预约) self.status new_status self.save()用状态机的好处是杜绝了已取消的预约又被改成已完成这类脏数据。别嫌这个逻辑简单等系统上线跑起来、数据量多的时候状态控制绝对是省心的一大块。4. 常见问题与排查经验4.1 数据库查询的 N1 问题开发时最容易踩的坑是列表页查询量爆炸。你在模板里遍历房源取owner.username或者house.images.all()每遍历一条就跑一次数据库100 条房源就是 100 次额外查询。解决方式是使用select_related针对外键和prefetch_related针对反向关联# 优化前 queryset House.objects.filter(statusactive) # 优化后 queryset House.objects.filter(statusactive).select_related(owner).prefetch_related(images)这个改动带来的速度提升是数量级的我实际测试中列表页响应时间从 1.8 秒降到 300 毫秒左右。判断是否有 N1 问题推荐装django-debug-toolbar这个神器它会在页面上直接显示数据库查询次数一眼看出哪里循环查了库。4.2 CSRF 与表单安全Django 默认开启了 CSRF 防护模板里用{% csrf_token %}即可。但我见过不少人用 Postman 测接口时没带csrftoken请求头导致 403以为是代码问题。排查方法很简单请求里带X-CSRFToken头值从 cookie 里取。另外一个安全点是所有表单都要做ModelForm验证而不是直接request.POST取值。Django 的 form 会处理字段类型转换、必填校验、长度限制还有很多 XSS 过滤是安全底线。4.3 图片上传的内存与存储优化上线后遇到过几次内存涨高的问题几乎都跟图片处理有关。Django 处理上传文件时默认会先读进内存超过 2.5MBFILE_UPLOAD_MAX_MEMORY_SIZE默认值才写临时文件。大图并发上来时内存直接顶不住。我处理的办法是Nginx 层限制client_max_body_sizeDjango 里封装统一的图片校验与压缩函数对 ImageField 的upload_to路径按日期分目录避免单目录文件过多生产环境静态文件和媒体文件分别处理媒体文件存到单独域名或 OSS。def handle_uploaded_image(file): img Image.open(file) img.thumbnail((800, 800)) buffer BytesIO() img.save(buffer, formatJPEG, quality85) buffer.seek(0) return ContentFile(buffer.read())4.4 Django Admin 搜索变慢Admin 列表里如果search_fields配的字段没有索引充其量只是慢一点但如果字段是icontains这类模糊搜索数据库没法用索引数据越多越卡。房源标题、地址这种字段也没法建普通索引来做模糊匹配。我的解法是给地址字段加上 MySQL 的全文索引Django 原生不支持得写RunSQL迁移或者干脆在 Admin 里减少模糊搜索字段只保留精确匹配类和状态类筛选。等房源到十万级别再考虑接入 ES。普通体量项目不需要在这上面过度纠结。4.5 时区问题Django 默认USE_TZ True数据库存的是 UTC 时间。用户看时间是北京时间必须在展示层做本地时间转换。推荐用django.utils.timezone.localtime输出模板里可以直接对 datetime 字段自动转。如果项目里有手动字符串拼接时间的代码一定要用localtime()包一下不然预约时间对不上用户投诉非常快。5. 部署要点与项目复盘5.1 上线前的检查清单项目部署阶段我整理了一个检查清单照着走基本能避免大部分事故DEBUG FalseALLOWED_HOSTS配置明确静态文件用collectstatic收集到指定目录数据库迁移文件全部执行并确认媒体文件路径有备份策略Gunicorn 或 uWSGI 配置了合适的 worker 数和超时时间Nginx 配置了 gzip 压缩、缓存过期时间、安全请求头定时任务如果有确认不会被重复触发。启动命令我用的 Gunicorngunicorn secondhand_home.wsgi:application \ --bind 127.0.0.1:8001 \ --workers 3 \ --timeout 60 \ --access-logfile /var/log/gunicorn/access.log \ --error-logfile /var/log/gunicorn/error.logWorker 数不是越多越好一般按 CPU 核心数 x21 来太多反而导致内存不够互相争抢。5.2 上线后的几个观察跑了两周之后我把监控数据拉了拉有几个结论90% 以上的查询集中在房源列表和详情页缓存设计到位之后数据库 QPS 很稳后台运营人员的操作量远大于 C 端用户说明后台顺手比前台好看更重要预约和成交的状态流转比起功能本身业务方更关心某一个客户到底到哪一步了。这提醒我交易流程模块的列表页一定要按用户和时间维度的组合查询做好。5.3 如果重做会在哪里改进如果重新从零做一遍有两个地方我会调整一是初期给所有外键字段统一加db_indexTrue并设计好组合索引避免上线后才发现慢查询。Django 的runserver模式下数据量小根本看不出性能问题等到正式环境才有压力。二是前端模板的组件化。当时用 Django 模板加 Bootstrap架构简单是简单但同一个组件比如房源卡片在列表页、推荐位、后台预览里被复制了好几份后面改样式要改很多地方。可以一开始就抽成include模板或者自定义模板标签维护起来省心很多。根据我个人经验像二手房交易这类传统业务系统技术难点通常不在某个单点功能而在大量看似琐碎的状态管理、权限控制和查询性能上。把基础模型的字段设计想清楚权限边界定义清楚查询链路优化到位整个系统就立得住。后续如果要做新房、租房甚至长租公寓业务基于这套结构去扩展成本很低这也是 Django 这类成熟框架给开发者的底气。