写PHP房屋租赁系统这套毕业设计,其实挺有意思的。它不像电商、社交平台那样烂大街,业务场景又足够复杂——用户角色有两种,交易的标的物是非标准化的房子,还有线上线下混合的看房流程要处理。做完一套下来,数据库设计、权限控制、文件上传、搜索排序、状态机流转这些基本功全都能覆盖到,用来做毕业设计或者项目经验展示,性价比很高。
这套系统的核心逻辑其实不复杂:房东发布房源,租客搜索、浏览、预约看房、下单、签约付款,管理员在后台做审核和统计。但真正动手写代码的时候,你会发现细节多到数不清:房子信息的字段要不要拆表、图片到底存服务器还是存数据库、订单状态怎么流转才不乱、用户权限怎么控制才不越权……这篇文章我就按实际开发顺序,把一套能跑通的PHP房屋租赁系统拆开讲清楚。如果你正准备做类似的毕设或者想自己练手,可以直接照着实操。
1. 项目整体设计与技术选型
1.1 需求来源与功能拆分
动手之前先想清楚做一个什么东西。房屋租赁系统是个典型的双边平台,一个用户既可以当租客去找房子,也可以当房东去发布房源。所以角色模型上要做成:普通用户(租客)、房东(发布者)、管理员(后台维护者)三种身份。常见做法是让一个用户表统一管理,靠level字段区分角色,这样做的好处是登录逻辑简单,后续如果一个租客想发布房源,也不需要单独再注册一次。
核心功能模块拆下来有这么几个:用户模块(注册、登录、找回密码、个人中心)、房源模块(发布、编辑、上下架、图片上传、房源列表、房源详情)、搜索模块(关键词搜索、区域筛选、租金区间、户型筛选、排序)、订单模块(预约看房、发起租赁、确认成交、取消订单、状态变更)、收藏模块(收藏房源、取消收藏)、后台管理(用户管理、房源审核、订单管理、统计报表)。
别觉得功能多,每个模块拆开看,本质上都是增删改查加一点业务逻辑。真正需要花心思的地方是几个关键点:房源图片怎么存储、订单状态怎么流转、搜索SQL怎么写才能兼顾效率和准确率。这些点我会在后面的章节逐个展开,这里先说一下为什么要这么设计。
1.2 技术栈:为什么选PHP而不是Java、Python、Node
毕设选PHP,首先得承认一个现实:你的核心目标是快速、稳妥地把系统跑通,把论文和答辩准备好,而不是折腾框架选型。PHP在这方面优势很明显,开发效率极高,环境和部署成本低,随便装一个集成环境就能跑。哪怕你导师要求你用MVC架构,现在的主流框架也都帮你把这些事情做好了。
这里我推荐用ThinkPHP 6或者Laravel,选这两个的原因很简单:首先中文文档和社区资料非常丰富,遇到问题搜一下就有答案;其次它们自带数据库ORM、模板引擎、路由解析、CSRF防护,这些功能自己从零写既浪费时间,又容易出安全漏洞。你要是问我原生的PHP行不行,也行,但前提是你得有足够的时间去处理SQL拼接、文件上传过滤、模板渲染这些非常琐碎的细节。做过一次之后你会发现,框架省下来的时间足够你把系统多打磨几轮。
如果非要在ThinkPHP和Laravel里选一个,我的习惯是毕业设计推荐ThinkPHP 6,理由很实在:它在国内校园场景下的参考资料更多,很多问题别人都踩过坑了,你遇到报错直接搜基本都能找到对应解法。Laravel的门槛略高,composer依赖、artisan命令行、Eloquent ORM这些概念对新手不算友好。技术选型这种问题上别意气用事,选你最能掌控的。
1.3 分层架构:MVC不是老古董
很多同学一听到MVC就觉得是老掉牙的东西,实际上对于一个带界面的Web系统,MVC的分层思想依然是最容易理解、最容易维护的。Controller层负责接收请求和响应,Model层负责数据操作,View层负责展示,逻辑清晰,别人看你代码的时候也能很轻松地顺着调用链路走。
这套系统的目录结构我习惯这样做:
- application / controller:存放控制器,按模块分组,比如Portal(前台)、Admin(后台)
- application / model:存放数据模型,比如UserModel、HouseModel、OrderModel
- application / view:存放模板文件,按控制器建子目录
- public / static:存放CSS、JS、图片等静态资源
写控制器的时候有一个很重要的原则:控制器只做参数接收、校验和结果返回,不要把业务逻辑全堆在控制器里。比如下单这个操作,正确做法是写一个OrderModel的createOrder方法,里面把库存校验、状态判断、订单生成这些逻辑封装起来,控制器只调用这一个方法,代码可读性和可维护性会提升好几个档次。这一点做得好,导师评阅代码的时候观感会明显不一样。
2. 数据库设计与核心表结构
2.1 用户、房源、订单三大主表设计
数据库设计是这套系统最见功力的地方。房屋租赁系统涉及的核心实体不算太多,但表与表之间的关联关系要理清楚。我一共设计了六张核心表:用户表、房源表、房源图片表、收藏表、订单表、管理员表。
用户表的核心字段要覆盖:用户名、密码(注意存的是哈希值,不是明文)、手机号、邮箱、角色等级、实名认证状态、注册时间。有个容易被忽略的点是用户在成为房东前是否要补全个人信息,这里我加了字段identity_status,0为未认证,1为已认证,发布房源时做校验。
房源表是最复杂的一张表,字段包括:发布者ID、标题、描述、租金、押金、面积、户型(几室几厅)、所在城市、区域、具体地址、经纬度(留给地图用的)、租赁方式(整租/合租)、状态(待审核/已上架/已出租/已下架)、点击量、发布时间。地址存到“区/街道”这一粒度就够了,具体门牌号建议只用文本字段保存,不要做结构化拆分,减少复杂度。
订单表需要理清楚租赁的业务流:用户浏览房源、预约看房、线下看房满意、线上下单、房东确认、签约付款、合同生效。我设计的订单表主要字段有订单号、房源ID、租客ID、房东ID、起租日期、结束日期、月租金、押金、总金额、状态、创建时间、更新时间。注意这里把房东ID也冗余进来了,虽然通过房源表能关联到房东,但订单查询时直接靠房东ID过滤效率更高,这就是典型的空间换时间。
2.2 状态机设计:订单流转如何避免脏数据
订单状态这块特别容易写崩,因为状态之间的迁移是有方向的,不是想改就改。我定义的状态集是:pending(待确认)、reserved(已预约)、signed(已签约)、active(履约中)、finished(已结束)、cancelled(已取消)、expired(已失效)。听起来很多,但摸清规律之后发现其实就三条线:正向走、反向取消、超时未处理。
正向走是正常流程:租客预约看房,订单进入reserved;看房满意后,租客确认租赁意向,状态改为signed;双方线下签合同(或者走线上电子合同),房东在后台确认,状态改为active;租期届满,状态改为finished。反向走是各种取消:租客未支付或主动取消,状态改为cancelled;房源在留定期内被其他租客抢先下单,原预约订单自动变为expired。
实现上有一个关键细节:状态变更不能只写一个UPDATE语句,而是要用“条件更新”来保证并发安全。也就是说,SQL语句的WHERE条件里必须带上当前期望的状态值,比如要把pending改成reserved,就写UPDATE order SET status = 'reserved' WHERE id = ? AND status = 'pending'。如果影响行数为0,说明订单状态已经被其他人改过了,这时候再做后续逻辑处理。这招对并发场景下的数据安全非常有效,比先查再改然后再写,靠谱得多。
2.3 数据冗余与索引优化
毕设阶段的数据量不大,没必要过度优化,但有几处设计习惯从开始就养好。第一,用户表、房源表、订单表的主键都用自增ID,不做联合主键,保持简单。第二,所有外键关系的字段都要建索引,比如house表的user_id、order表的house_id、order表的renter_id,因为查询基本都是靠这些字段过滤的。
第三,搜索场景要建联合索引。比如搜索房源最常见的过滤条件是city+status+rent,那么就可以建一个(city, status, rent)的联合索引。要注意索引顺序,最左前缀原则决定了条件字段的顺序要和索引顺序一致。实际操作中我发现,毕设的数据量下索引实战意义不大,但把这个概念写进论文和答辩中是加分项,导师都会认可你考虑到了这个问题。
关于数据表的一个经验是:不要为了省事把图片URL拼成一个长字符串存在房源表的一个字段里。正确做法是单独建一张house_image表,一个房源多条图片记录,字段包括id、house_id、image_url、sort_order。这样做的好处是后续做轮播图、封面图、图片管理都很灵活,SQL拆分组装起来也直观。
3. 核心功能模块解析与实现细节
3.1 登录认证与权限控制
登录认证这部分,我的建议是使用PHP框架自带的Session机制,而不是自己去写JWT。原因很简单:Session在传统PHP项目中天然得好,服务端存储,浏览器的Cookie自动带上Session ID,配合框架的Auth组件可以很方便地实现用户状态的保持。JWT虽然在这几年很流行,但它在无状态接口服务上更有优势,这种传统的页面+表单的Web系统用Session就够了。
如果用的ThinkPHP 6,内置的Session功能非常完善,我把登录流程串一下:用户提交用户名和密码,控制器先做基础校验(字段是否为空、格式是否正确),然后调用UserModel的checkLogin方法,方法内部用哈希校验密码,成功之后把用户ID和角色信息写入Session,同时更新用户的最后登录时间。前端页面通过判断Session里的user信息来决定是否显示登录入口。
权限控制分两层:第一层是页面级的,通过控制器基类里加一个初始化方法,比如admin模块的控制器都继承同一个AdminBase控制器,只要请求进入到这个模块,先检查当前Session里有没有管理员的信息,没有就跳转到登录页。第二层是操作级的,比如普通用户不能直接调用后台接口修改其他用户的数据,这需要在关键业务方法里做归属校验:如果数据当前的owner_id不等于Session里的uid,直接返回错误。
3.2 房源发布与图片上传
发布房源是整个系统最核心的写操作,牵扯到的字段多、关联表多,最容易出问题。我把这个流程拆成三步:表单提交基础信息、上传房源图片、保存关联数据。前端用了一个多字段表单,包含标题、租金、面积、户型、地址、描述等,图片上传用异步方式,选择完图片之后立即上传到服务器,返回图片URL,最后提交表单时把URL数组一起提交。
图片上传有个点要非常注意:PHP默认的upload_tmp_dir路径下,上传的文件是临时文件,必须在请求结束前用move_uploaded_file函数移动到指定目录,否则就会被自动清理。安全方面要做几个校验,一是扩展名白名单,只允许jpg、jpeg、png、gif;二是MIME类型检查;三是文件大小限制,我建议单张不超过2MB,在php.ini的upload_max_filesize里设置。还要注意生成新的文件名,不要用用户上传的原始文件名,直接时间戳加随机数拼接,防止路径穿越和重名覆盖。
存储路径我建议放到public/static/upload目录下,并且在保存数据库时只存相对路径,比如/static/upload/20250612_xxxx.jpg。这样做的原因是一旦部署上线,把整个项目放到Web根目录,图片就能直接通过URL访问,不需要额外的静态文件路由。如果你把图片存在了application目录下的某个自定义目录里,部署的时候很容易出现路径不对、无法访问的情况。
3.3 搜索筛选与分页
搜索模块看起来简单,实际上是最容易写出“面条代码”的地方。用户选的筛选项可能是组合的:城市、区域、租金范围、户型、租赁方式、排序方式。如果每多一个条件就拼一个字符串,代码会变得又长又乱。我习惯用条件数组的方式来拼SQL,核心思想就是把所有筛选条件统一装进一个数组里,最后统一构造WHERE条件。
还是以ThinkPHP为例,先初始化一个查询对象,然后逐个条件追加:如果传了city,就where city = $city;如果传了min_price,就where rent >= $min_price;如果传了max_price,就where rent <= $max_price;户型条件可能是几室,那就where layout like %三室%。排序方面预留几个选项,默认按发布时间倒序,也支持按租金升序、降序。注意租金排序时数据库里存的是整数类型,不要用字符串字段存金额,否则排序结果会非常怪。
分页用框架自带的分页类就够了,传入每页条数和当前页码,查询结果返回一个包含数据列表和分页HTML的数据结构,模板里直接输出分页链接。有个小坑是分页链接在搜索结果页会丢失查询参数,所以生成分页链接时一定要把当前的搜索条件拼进去,比如page/2/type/1/area/xxx这种格式,否则点击第二页之后搜索条件就全部丢了。这个细节很多人第一次写都没注意。
3.4 订单流程与合同生成
订单流程是这套系统的业务核心,我前面说了状态机,这里展开讲具体实现。用户看中一套房,首先发起预约看房请求,系统自动生成一个预约订单,状态为reserved,同时给房东发一条站内消息。之后租客实地看房,满意后确认租赁,订单状态改为signed,此时系统要做一次并发检查,确认房源状态仍是“可租”;如果房源已经被其他人抢先下单,就友好提示租客重新选择。
房东端接收到已签约订单后,可以确认并生成合同。合同这块我建议直接生成PDF文件,技术方案选PHPExcel库的扩展或者TCPDF库都行,模板里拼上租客姓名、身份证号、房源地址、租金、押金、租期这些字段,生成后保存到本地目录,同时在订单表里记录合同文件的路径。生成PDF的好处是答辩演示的时候直接把合同导出来展示,效果比口头讲好很多。
订单列表页要多加一个筛选功能,按状态标签分页显示。租客端可以看到自己发起的订单和各自的状态,房东端可以看到所有与自己房源相关的订单。注意列表里不要一次性把房源的所有字段都查出来,只需要冗余的关键字段,比如封面图、标题、租金、地址,这种冗余字段在生成订单的时候就直接复制到订单表里面存一份,后续订单状态再怎么变,这些快照信息都不会受影响。
3.5 后台管理功能
后台管理模块是区别于普通前端页面的独立子系统,我的做法是把后台路由统一加上admin前缀,比如/admin/login、/admin/index、/admin/house/list,然后整个admin模块的控制器都继承AdminBase控制器做权限控制。后台功能不需要很花哨,但必须把关键的几个管理入口打通:用户管理、房源管理、订单管理、数据统计。
房源管理里最要紧的是审核功能。会员发布的房源默认是待审核状态,管理员在后台查看房源详情后选择通过或驳回,驳回时需要填写原因,前端页面会显示这个原因,让发布者知道该改哪里。订单管理主要是一个可搜索的订单列表,管理员能看到所有订单的状态,可以对个别异常订单进行手动关闭处理。数据统计稍微做几个简单的指标就行:总用户数、总房源数、总订单数、本月新增用户数、本月成交订单数,画个柱状图或者折线图。图表不建议用复杂的前端库,用ECharts就够了,引入一个JS文件渲染一下,效果已经非常好看。
4. 实操过程:从零搭建到跑通
4.1 开发环境准备
在本地开发,我推荐直接用集成环境把PHP、MySQL、Apache/Nginx一次性装好,Windows上用phpstudy,Mac上用MAMP,非常省事。PHP版本选7.4或者8.0都行,ThinkPHP 6要求PHP >= 7.2.5,Laravel 9要求PHP >= 8.0,选个目前主流的版本就行。MySQL这里用5.7,注意MySQL 8.0的认证插件跟PHP的老版本连接方式有兼容性问题,如果非要装8.0,记得把认证规则改成mysql_native_password,否则连数据库会报错。
数据库用phpMyAdmin或者Navicat都行,我习惯先用SQL语句把数据库和表结构创建出来,再往里面灌测试数据。测试数据很重要,房源数据至少准备20条,用户数据至少10条,不然搜索结果页空荡荡的,演示效果很糟糕。写个PHP脚本循环插入随机数据也行,手动分批插入也行,关键是字段之间逻辑保持一致,比如房屋状态是已租,就不要再出现在可租列表里。
4.2 项目初始化与目录结构
如果你选了ThinkPHP 6,初始化项目很简单,composer create-project topthink/think tp6这样的命令就能拉下来一个空项目。如果你对composer有点抗拒,也可以直接从官网下载完整的源码包,解压就能用。初始化完成后先把数据库连接配置改好,在.env文件里填入数据库名、用户名、密码,然后确认项目能正常访问首页,这一步跑通了,后面所有开发都在这个地基上。
接下来要规划好目录结构。Controller目录下面按模块分子目录,Portal放前台控制器,Admin放后台控制器,每个控制器对应一个主要的业务实体。Model目录下面放和表对应的模型类,一个表对应一个模型,模型里写这个表相关的业务逻辑方法。View目录下面按控制器建目录,一个控制器对应一个文件夹,里面的HTML文件按方法名命名。这样的目录规划虽然简单,但让我后面找人帮我维护代码或者自己隔了一周再看代码,都不会迷路。
4.3 前端页面与PHP混合开发的实现
这套系统的前端我没有用前后端分离,直接用ThinkPHP的模板引擎在服务端渲染HTML。原因很简单:前后端分离意味着你要同时维护一套前端工程和一套接口服务,对毕设来讲工作量翻倍,而且答辩时你还需要解释为什么引入这么复杂的架构。PHP混编的方式,代码都在一个项目里,部署只需要一份,展示逻辑也直白。
模板文件里可以用框架自带的模板语法,比如遍历房源列表就用foreach循环,填充表单的默认值就用value="{$house.title}"。这种模式下的页面刷新是整页刷新,交互体验肯定不如Vue、React这类前端框架流畅,但作为一个管理系统需求,完全够用。要注意页面的统一风格,我建议把头部导航栏、底部版权信息抽成公共模板文件,用模板继承或者include的方式引入,比如layout.html放公共头部和尾部,各个页面只写主体部分的内容。
4.4 接口联调与数据填充
虽然没做前后端分离,但前后台交互的过程中仍然涉及到一些接口调用,比如异步图片上传、注册时检测用户名是否重复、收藏房源时不用跳页。这些功能我用jQuery的Ajax实现,控制器返回JSON格式数据,前端根据返回的code字段判断成功还是失败,弹出对应的提示信息。注意控制器返回JSON之前要设置好响应头,并且关闭模板渲染,否则框架可能返回一个HTML页面而不是JSON结构。
数据填充这一环节别偷懒,我前面说过至少要保证演示时页面是丰满的。除了手动准备数据,还可以写一个数据填充脚本,放在database/seed目录下,直接调用模型批量生成测试数据。这样你电脑重装环境或者数据库被我搞崩了之后,一条命令就能把演示数据全部恢复。尤其是答辩前,万一数据库出了意外,有这个脚本能救你一把。
5. 常见问题与排查技巧实录
5.1 中文乱码问题
中文乱码是最常见、最容易排查的一个问题,但很多新手绕不明白。这个问题的本质是字符集不一致,到处都在用编码,最关键的三个环节保持一致就不会乱:数据库表字符集是utf8mb4、PHP文件保存编码是UTF-8、页面输出字符编码是UTF-8。其中两个点特别容易被忽略:一是连接数据库的DSN里要指定charset=utf8mb4;二是HTML页面的meta标签写的是charset=utf-8,但PHP文件本身可能是ANSI编码保存,这时候输出到浏览器必然乱码。排查思路很简单,用浏览器开发者工具看响应头里的Content-Type,再看数据库里的数据到底有没有变成乱码,先定位是存储环节还是读取环节出了问题。
5.2 图片上传失败
图片上传失败的原因无非几种:文件过大被php.ini的upload_max_filesize限制、上传目录没有写权限、文件扩展名校验不通过、move_uploaded_file函数执行时目标目录不存在。前两个问题都可以通过检查php.ini配置和目录权限来排查,第三个问题通过打印上传错误码来定位,第四个问题是很多人容易错过的:上传目录要提前创建好,TP框架里一般用递归创建目录的方法保证目录存在后再移动文件。我建议在upload方法里加上对应的错误码日志记录,上传失败时把错误码写进日志,排查效率高很多。
5.3 SQL注入与安全防护
这个话题我觉得就是“渐进式”的:先把框架提供的基础防护做扎实,再在关键环节手动加固。使用ThinkPHP这类框架的查询构造器和ORM,方法内部已经做了参数绑定,基本的注入风险已经被挡掉了,但有几个坑是框架管不了的:一是原生SQL查询,如果你因为某些场景用了query方法执行SQL字符串,里面的参数一定要手动加引号处理;二是搜索模块的动态排序,如果把用户传进来的排序字段直接拼进SQL,攻击者可以传一个奇怪的order字段,虽然危害有限,但属于代码缺陷。安全习惯上,后台登录要加验证码,接口要加CSRF Token校验,文件上传要过滤MIME类型,密码不能明文存储,可以用password_hash函数处理。
5.4 分页参数错误
分页参数出错主要发生在两个地方:一是页码参数传成字符串导致SQL报错,这个只需要做一次强制类型转换(intval)就能解决;二是搜索结果分页丢失查询条件,这个我前面提过,要手动把查询参数拼到分页链接中。还有一个小细节是分页跳转到超出范围页码时,要判断一下总页数,如果当前页大于总页数就强制跳转到第一页或者最后一页,不要让用户看到空白页。
5.5 部署到生产环境时容易踩的坑
本地跑得好好的,一到线上就出问题,这是所有新手都会经历的过程。我总结下来主要有几个原因:第一,文件目录权限。线上服务器的Web用户默认是www-data或者nginx,你需要保证storage目录、upload目录、runtime目录对Web用户可写。第二,伪静态配置。ThinkPHP默认的URL模式需要服务器把请求重写到入口文件的index.php,Apache要开启mod_rewrite,Nginx要配置try_files规则,这个不配好,访问除首页之外的任何页面都会404。第三,环境变量。线上部署一定要把.env文件里的debug模式改为false,关闭错误堆栈显示,否则SQL报错信息会直接暴露给访客,样子也不好。
6. 个人经验总结与扩展建议
6.1 做完这套系统的几点心得体会
如果你也在做PHP相关的毕设,我觉得有几个经验提前知道能少走很多弯路。第一,项目能跑通只是起点,业务逻辑的正确性和代码的可读性才是拉开差距的地方。比如订单状态机设计、上传文件的校验、查询条件的动态拼装,这些点做得稳,答辩时导师问起来你能解释,就是加分项。第二,数据库设计阶段多花时间,比代码阶段反复改SQL要划算得多。我一开始设计房源表的时候忘了把收藏功能和订单表的关联字段想清楚,后面补字段、改数据接口,浪费了将近一周的精力。
第三,演示数据一定要丰富。答辩的时候导师觉得房源列表有20多条数据、搜索能筛选、分类能看到不同频道、订单能流转起来,直接锤实了系统的完成度。如果列表页只有两三条假数据,哪怕后端逻辑再严谨,观感上也像一个半成品。
6.2 后续扩展方向
这套系统的核心骨架已经足够稳了,想继续扩展,可以考虑几个方向。如果对前端感兴趣,可以把前台首页和房源列表页改成Vue + API的方式,把PHP端改造成纯粹的API服务,这就算完成了一次前后端分离升级,简历里能写的东西又多一条。如果想把这个项目做成一个真实的SaaS产品,可以继续加支付功能(接入第三方支付的电脑网站支付、H5支付)、电子合同在线签署、在线客服、定期自动生成账单、房源VR看房这些模块。
我个人更建议的是业务层面做深,比如增加一个“房东评分”体系,用户在交房之后可以对房东进行评价,然后把这部分数据结构加入现有的订单流程中。原因是这个扩展会让系统的数据模型更复杂,业务闭环更完整,也更能体现你对系统全局的把控能力。
最后再分享一个写这类管理系统的小技巧:每完成一个模块的功能,就顺手把对应的SQL语句、关键配置文件、session交互流程整理成一个文档,放到项目的docs目录下。写论文的时候你会发现这些东西直接就能用,答辩准备PPT的时候也能直接拉出流程图和数据字典,不用重新翻代码去回忆当初是怎么设计的。