做了几年房产行业的信息化项目,手头这套用ThinkPHP加Vue搭起来的房屋房产销售信息管理系统,算是我个人比较满意的一个作品。说实话,房产销售行业的软件需求一直很特殊,它不像纯电商或者纯内容管理那样有现成的模板可以套,很多业务流程的细节都需要定制化开发。今天把这套系统的设计与实现思路整理出来,从需求分析、技术选型到前后端核心代码,再到部署上线遇到的坑,一次性讲清楚,希望能给正在做类似系统或者准备入行房产软件开发的同行一些参考。
这个系统的核心场景很明确:一个房产中介公司或者开发商销售部,需要把房源、客户、带看、成交这整条业务线管起来。传统的做法是Excel加微信,房源多了之后,经常出现房源重复登记、客户跟进记录丢失、佣金算错这些乱七八糟的问题。这套系统就是要把这些线下流程搬到线上,用ThinkPHP做后端API接口,用Vue做前端页面,实现房源管理、客户管理、带看记录、成交管理和数据统计这几个核心功能模块。适合有一定PHP和前端基础、想做一个完整前后端分离项目的开发者参考,也适合房产行业的技术人员了解这类业务系统到底是怎么设计的。
1. 项目整体设计与思路拆解
1.1 房产销售业务的核心痛点
在动手写代码之前,我花了不少时间泡在房产门店里,观察销售人员和店长的日常工作。发现他们的痛点非常集中:
- 房源信息不统一:同一个房源可能被好几个业务员重复录入,照片、价格、户型信息甚至对不上,客户问起来销售自己都含糊。
- 客户跟进没记录:今天给哪个客户打了电话,约了明天几点看房,全靠业务员个人记忆,人一离职,客户资源跟着就带走了。
- 带看过程无迹可循:什么时候带客户去看的房,看完之后客户反馈怎么样,价格谈判到了哪一步,管理层完全不清楚。
- 成交数据统计滞后:月底统计佣金、算业绩的时候,只能靠人工翻记录,又慢又容易出错。
所以这套系统的第一条设计原则,就是以房源为中心、以客户为主线,把每一套房源的完整状态和每一个客户的跟进过程全部记录下来。第二条原则是权限分层:业务员只能看自己的客户和房源,店长可以看整个门店的,老板可以看所有数据。这个权限设计在后端接口和前端路由两个层面都需要执行。
1.2 系统模块规划与功能边界
整个系统我划分了六大功能模块:
| 模块名称 | 核心功能 | 主要角色 |
|---|---|---|
| 系统管理 | 用户管理、角色权限、操作日志 | 管理员 |
| 房源管理 | 房源录入、审核、上下架、条件检索 | 业务员、店长 |
| 客户管理 | 客户登记、跟进记录、意向标签 | 业务员 |
| 带看管理 | 预约带看、带看记录、反馈回写 | 业务员、店长 |
| 成交管理 | 成交登记、合同信息、佣金计算 | 店长、销售 |
| 数据统计 | 房源/客户/成交多维度统计 | 店长、老板 |
功能边界一定要划清楚,这是我在做过多个管理类系统之后最深的体会。最开始我差点把财务功能也塞进去,后来想明白了一个道理:房产销售系统里涉及到钱的部分,比如佣金结算、财务流水,最好不要自己造轮子,跟财务软件对接或者单独做模块会更稳妥。贸然扩展功能边界,会让系统的复杂度和风险成倍上升,后续维护也会非常痛苦。
1.3 为什么选择ThinkPHP + Vue这套组合
选型的时候我在几个方案之间犹豫过。Spring Boot + Vue功能强大,但PHP团队维护起来有难度;Django+Vue也不错,但国内房产行业相关的PHP生态资料更多,招人也更容易。最终选了ThinkPHP + Vue,理由很实在:
- ThinkPHP 6.0 LTS版本稳定性好:经过这么多年的迭代,框架的核心代码已经非常成熟,社区活跃度在国内数一数二,遇到问题搜一下基本都能找到解决方案。项目用到的数据库操作、缓存、验证器这些功能,ThinkPHP都内置了,不需要引入太多第三方包。
- Vue的渐进式开发体验:Vue的前端生态对中小型管理系统的适配度极高,Element UI等组件库开箱即用,表单、表格、弹窗这些后台管理系统的通用界面,基本不用写太多重复代码。而且Vue的文档和社区资料是所有前端框架里最丰富的,团队新成员上手非常快。
- 前后端分离是必然趋势:虽然ThinkPHP本身也能渲染模板,但既然要做成管理后台,前后端分离能更好地支撑以后可能的移动端、小程序端的接入。后端只管输出JSON,前端专注交互,接口定义好了两边并行开发,效率比传统MVC高很多。
热词里很多人都在搜“thinkphp v6.0.12lts”和“vue安装及环境配置”,说明大家确实是想用这套组合做实际项目,但第一步就被环境卡住了。后面我会把整个环境的搭建过程完整写一遍,包括我踩过的那些细节坑。
2. 开发环境准备与项目初始化
2.1 本地环境搭建的版本选择
这套系统的开发环境我在两台机器上分别配置过,一台是Windows,一台是Mac,版本组合如下:
- PHP:8.0以上(ThinkPHP 6对PHP 8的支持已经很完善,可以用上JIT等新特性)
- 数据库:MySQL 5.7或8.0(推荐8.0,JSON字段处理更强大)
- Web服务器:本地开发用PHP内置服务器,正式环境用Nginx
- 前端:Node.js 16以上,npm 8以上
- PHP依赖管理:Composer 2.x
具体安装过程这里不一步步截图了,重点说几个坑。第一,PHP8.0以后,很多老的PHP扩展需要单独确认是否已启用,比如fileinfo扩展是ThinkPHP的验证器依赖的,默认没开的话会出现文件上传验证失败的诡异问题。第二,Composer安装ThinkPHP的时候,如果网络不稳定会超时,建议先配置好Composer的中国镜像源。
2.2 ThinkPHP 6后端项目的创建与配置
用Composer创建项目是标准操作:
composer create-project topthink/think tp_house创建完成之后,第一步是修改.env文件,配置数据库连接:
APP_DEBUG = true [APP] DEFAULT_TIMEZONE = Asia/Shanghai [DATABASE] TYPE = mysql HOSTNAME = 127.0.0.1 DATABASE = house_sale USERNAME = root PASSWORD = your_password HOSTPORT = 3306 CHARSET = utf8mb4 DEBUG = true这里要强调一个细节:ThinkPHP 6默认使用php think run启动开发服务器,URL访问格式是http://localhost:8000。但前后端分离项目里,前端Vue访问后端接口会涉及跨域问题。我建议开发阶段直接在后端配置跨域中间件,后面会在接口章节详细讲。
2.3 Vue项目的脚手架搭建
前端我用的Vue 3.4版本搭配Vite构建工具。很多教程还在推Vue CLI,但新项目强烈建议直接用Vite,启动速度快一个量级,热更新体验也好很多。
npm create vite@latest house-admin -- --template vue cd house-admin npm install npm install vue-router@4 pinia axios element-plus注意这里我直接一次性把项目会用到的依赖全装上了:vue-router是前端路由,pinia是状态管理库(Vue 3官方推荐,替代Vue 2时代的Vuex),axios负责HTTP请求,element-plus是UI组件库。这套依赖组合是当前Vue 3项目最主流的技术栈搭配。
装完之后,用VS Code打开项目,推荐安装Volar插件而不是老旧的Vetur,Volar对Vue 3的组合式API和TypeScript支持更好。热词里有人搜“vscode配置vue开发环境”,重点就看两个设置:一是文件关联确认.vue文件用Volar接管,二是打开vue文件的自动格式化功能,用Prettier统一风格,不然团队协作时格式冲突会让人崩溃。
3. 数据库设计与ThinkPHP后端实现
3.1 核心数据表结构设计
数据库设计是这类业务系统的地基,我在这里花的精力最多。整个系统一共12张表,核心的表有6张。
首先是house房源表,字段包括:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| title | varchar(200) | 房源标题 |
| district | varchar(50) | 所属区域 |
| address | varchar(255) | 详细地址 |
| house_type | varchar(20) | 户型,如3室2厅 |
| area | decimal(10,2) | 建筑面积 |
| total_price | decimal(12,2) | 总价 |
| unit_price | decimal(12,2) | 单价 |
| status | tinyint | 状态:1在售 2已定 3已售 4下架 |
| owner_id | int | 业主ID |
| is_audit | tinyint | 审核状态 |
| create_time | int | 录入时间 |
这里有一个设计要点:status和is_audit必须分开。之前有个同行把审核状态和出售状态混在一个字段里,结果想筛选“已审核的在售房源”时发现SQL写得很别扭,还容易出逻辑漏洞。另外,总价和单价要同时冗余存储,虽然单价可以由总价除以面积算出来,但统计报表的时候每次都实时计算,高并发下数据库压力很大,冗余字段换查询性能,在这个场景下是划算的。
然后是customer客户表,加了intention_level意向等级字段,用1-5的整数表示,在客户筛选排序时能派上大用场。follow_record跟进记录表是我特别设计的:每个客户可以有多条跟进记录,每条记录包含跟进方式(电话、微信、到店、带看)、跟进内容、下次跟进时间三个关键信息。这个设计让整个客户跟进过程有完整的时间线,管理层随时能审查业务员的工作质量。
带看记录表visit_record关联了房源、客户、业务员三方,加上客户反馈和价格谈判情况。成交表deal_record则记录了最终的成交价、佣金比例、合同编号,是后续统计的核心数据来源。
3.2 创建数据表和基础模型
表结构在MySQL命令行或Navicat里执行SQL建表即可,这里分享一个ThinkPHP 6的模型创建技巧。数据表建好之后,用命令行工具快速生成对应的模型类:
php think make:model House php think make:model Customer php think make:model VisitRecord生成的基础模型类文件在app\model目录下,继承think\Model。我在模型层主要做了三件事:
第一,定义表关联。比如House模型关联Owner业主信息、VisitRecord带看记录:
public function owner() { return $this->belongsTo(Owner::class, 'owner_id', 'id'); } public function visits() { return $this->hasMany(VisitRecord::class, 'house_id', 'id'); }第二,定义字段类型自动转换。ThinkPHP 6支持在模型中直接指定字段类型,格式化输出的时候省去很多麻烦:
protected $type = [ 'area' => 'float', 'create_time' => 'timestamp:Y-m-d H:i', ];第三,使用全局作用域做软删除和状态过滤。房源删除采用软删除机制,调用destroy()方法并不会真正删除数据库记录,而是在delete_time字段写入时间戳。这是为了防止业务员手滑删掉重要房源导致后续扯皮。
3.3 认证机制与权限控制的实现思路
登录认证用的JWT方案。ThinkPHP 6官方没有内置JWT,我引入了firebase/php-jwt这个扩展包。整体认证流程是:用户提交用户名密码,后端验证通过后签发一个有效期为8小时的JWT令牌返回给前端;前端把令牌存在localStorage里,每次请求在Authorization请求头上带上;后端在中间件里解析令牌获取用户信息。
关键的后端验证代码是这样的:
public function parseToken(Request $request) { $header = $request->header('Authorization'); if (!$header || !preg_match('/Bearer\s(\S+)/', $header, $matches)) { throw new HttpException(401, '未登录或登录已过期'); } try { $decoded = JWT::decode($matches[1], env('JWT_SECRET'), array('HS256')); return $decoded; } catch (Exception $e) { throw new HttpException(401, 'Token无效或已过期'); } }权限控制用了ThinkPHP的后置中间件机制,自定义一个CheckAuth中间件,在每个需要登录的控制器模块里注册。角色权限在数据库里做了role字段标识,1是管理员,2是店长,3是业务员。注意权限控制不能只靠前端隐藏按钮,接口层必须有校验,否则懂点技术的人直接调接口就能绕过限制了。
3.4 房源管理核心接口实现
房源模块是系统的重头戏,我实现了三个核心接口:房源新增、房源列表查询(带多条件筛选)和房源状态变更。
房源新增接口的核心逻辑:
public function save(Request $request) { $data = $request->post(); validate(\app\validate\HouseValidate::class)->check($data); $house = new House(); $house->title = $data['title']; $house->district = $data['district']; // 其他字段赋值 $house->status = 1; $house->is_audit = 0; $house->create_time = time(); $house->save(); return json(['code' => 0, 'msg' => '提交成功,等待审核', 'data' => ['id' => $house->id]]); }这里用了ThinkPHP的验证器,严格校验必填字段和数据格式。这一步特别重要,我见过太多系统因为后端没有做参数校验,前端传了个负数面积进来,后面的统计报表全是负数,查问题查到崩溃。
房源列表查询是多条件组合筛选的重点难点。用户可能按区域、价格区间、户型、面积、状态等多个条件组合筛选,我一开始写的SQL拼接代码非常混乱,后来重构封装成了查询构造器链式写法:
public function getList(Request $request) { $page = $request->get('page', 1); $limit = $request->get('limit', 10); $where = []; if ($district = $request->get('district')) { $where[] = ['district', '=', $district]; } if ($status = $request->get('status')) { $where[] = ['status', '=', $status]; } if ($minPrice = $request->get('min_price')) { $where[] = ['total_price', '>=', $minPrice]; } if ($maxPrice = $request->get('max_price')) { $where[] = ['total_price', '<=', $maxPrice]; } $list = House::with(['owner']) ->where($where) ->order('create_time', 'desc') ->paginate([$limit, 'page' => $page]); return json(['code' => 0, 'data' => $list]); }ThinkPHP的paginate方法会自动处理分页参数,返回的数据带上总条数和分页信息,前端表格组件可以直接用。
还有一个关键的SQL监听技巧,热词里有人在搜“thinkphp 监听sql的代码一般添加在哪里”。如果SQL执行有问题,可以在应用的app/middleware.php里注册一个全局中间件,或者在AppService中监听事件。我用的办法最简单粗暴,调试阶段在.env里设置DATABASE.DEBUG = true,然后安装tracy/tracy调试工具栏,页面底部会展示执行的所有SQL语句,定位慢查询和错误查询非常方便。正式环境记得关掉这个开关,不然SQL语句全暴露在报错页面里,有安全风险。
3.5 前后端分离的跨域处理方案
跨域是前后端分离项目开发阶段的第一个拦路虎。Vue项目跑在http://localhost:5173,ThinkPHP接口跑在http://localhost:8000,端口不同就产生了跨域。
有两种解决方案。开发阶段的推荐方案是配置Vite的DevServer代理,在vite.config.js里设置:
export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } } } })这样前端代码里请求/api/house/list,Vite开发服务器会自动转发到http://localhost:8000/api/house/list,浏览器的请求源就没变,跨域问题在源头就化解了。这个方案的好处是不用在后端处理CORS,生产环境Nginx也可以用同样的思路处理。
如果确实需要后端开启跨域,可以在ThinkPHP中间件中设置响应头:
public function handle($request, \Closure $next) { $response = $next($request); $response->header([ 'Access-Control-Allow-Origin' => '*', 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers' => 'Authorization, Content-Type', ]); return $response; }生产环境我建议用Nginx来处理跨域,后端保持干净,后面部署章节会讲到。
4. Vue前端核心实现
4.1 前端技术方案与工程结构
前端项目我使用了Vue 3的组合式API(Composition API)开发。热词里很多人问“vue选项式和组合式区别”,我用实际项目经验来解释:选项式把逻辑分散在data、methods、computed这些固定字段里,组件小的时候很清晰,但业务逻辑一复杂,一个功能的代码会被拆散到好几个选项里,维护起来很痛苦;组合式API用setup语法,可以把同一个业务逻辑的所有代码封装在computed、watch、methods等函数的组合里,按功能域组织逻辑代码,复用性也更好。做房产销售这种业务逻辑比较重的系统,我强烈建议直接用组合式API。
前端项目的目录结构:
src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 ├── views/ # 页面组件 ├── utils/ # 工具函数 ├── App.vue └── main.js4.2 Axios封装与Token处理
axios封装是整个前端代码里复用率最高的部分。热词里有“vue前后端分离请求token处理”、“vue axios devserver转发”,这两个问题我都遇到过,这里给出完整方案。
在utils/request.js中统一封装:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:注入Token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理错误 request.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response?.status === 401) { ElMessage.error('登录已过期,请重新登录') localStorage.removeItem('token') router.push('/login') } else { ElMessage.error(error.message || '网络异常') } return Promise.reject(error) } ) export default request这套封装的核心价值在于:第一,Token的注入不需要每个页面手动写;第二,响应拦截器已经处理了后端统一的{code, msg, data}格式,页面里拿数据直接res.data就行,不用重复做错误处理;第三,所有地方都能拿到统一的401拦截逻辑,登录过期自动跳转到登录页。
Token失效的处理是我在实际运营中最常被问到的。有用户问“为什么我登录一会儿就退出了”,排查发现是Token有效期设置太短。JWT的有效期一般8个小时比较合理,跟实际业务场景匹配,业务员早上登录下班前应该还在有效期内。但要注意刷新页面的场景,Token存localStorage虽然简单,但刷新页面不会丢,这点比内存存储好,所以最终选择存localStorage,安全性方面用HTTPS部署来兜底。
4.3 前端路由设计与权限控制
Vue Router一共设计了四个层级的路由:登录页独立出来,主布局下面是首页看板、房源管理、客户管理、带看管理、成交管理、统计报表、系统设置。
路由配置核心部分:
const routes = [ { path: '/login', component: () => import('../views/Login.vue') }, { path: '/', component: () => import('../layouts/MainLayout.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('../views/Dashboard.vue'), meta: { title: '首页看板' } }, { path: 'houses', component: () => import('../views/house/HouseList.vue'), meta: { title: '房源管理', roles: ['admin', 'manager'] } }, { path: 'customers', component: () => import('../views/customer/CustomerList.vue'), meta: { title: '客户管理' } }, // ... 其他路由 ] } ]这里用到了路由懒加载(() => import()形式),首屏加载速度会快很多,算是一个比较基础但非常有效的性能优化措施。
前端权限控制通过路由守卫实现:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') return } if (token && to.path === '/login') { next('/') return } // 这里还可以根据用户角色做路由过滤 next() })路由守卫有两个作用:没登录的用户不管访问什么页面,都会被弹回登录页;同时配合后端接口的权限校验,实现双重保障。
4.4 房源管理页面与认证交互实现
用户登录认证页面是前端的一个重点模块。登录页的核心代码:
<template> <div class="login-page"> <el-form ref="loginFormRef" :model="loginForm" :rules="loginRules"> <el-form-item prop="username"> <el-input v-model="loginForm.username" placeholder="用户名" /> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" type="password" placeholder="密码" /> </el-form-item> <el-button type="primary" :loading="loading" @click="handleLogin">登录</el-button> </el-form> </div> </template> <script setup> import { reactive, ref } from 'vue' import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { loginApi } from '../api/user' import { useUserStore } from '../stores/user' const router = useRouter() const userStore = useUserStore() const loginForm = reactive({ username: '', password: '' }) const loading = ref(false) const handleLogin = async () => { loading.value = true try { const res = await loginApi(loginForm) localStorage.setItem('token', res.data.token) localStorage.setItem('userInfo', JSON.stringify(res.data.user)) userStore.setUser(res.data.user) ElMessage.success('登录成功') router.push('/') } finally { loading.value = false } } </script>这页代码看起来不难,但有几个细节是踩过坑之后才总结出来的。登录按钮必须加loading状态,防止用户重复点击导致重复提交;登录成功之后不要忘了把用户角色信息也存下来,后面路由守卫和菜单栏权限显示都要用;错误提示用ElMessage统一弹出,比浏览器默认alert美观很多,也不会阻塞界面。
房源列表页的前端实现,主要用Element Plus的表格组件加载后端数据:
<el-table :data="houseList"> <el-table-column prop="title" label="房源标题" min-width="200" /> <el-table-column prop="district" label="区域" width="100" /> <el-table-column prop="house_type" label="户型" width="100" /> <el-table-column prop="area" label="面积(㎡)" width="100" /> <el-table-column prop="total_price" label="总价(万)" width="100" /> <el-table-column label="状态" width="100"> <template #default="{ row }"> <el-tag :type="statusMap[row.status].type">{{ statusMap[row.status].label }}</el-tag> </template> </el-table-column> <el-table-column label="操作" width="200" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openDetail(row)">详情</el-button> <el-button link type="warning" @click="editHouse(row)">编辑</el-button> </template> </el-table-column> </el-table>筛选条件部分用了el-select和el-input组件,点击“搜索”按钮后重新请求接口,点击“重置”清空条件恢复默认列表。这里要注意前端把筛选参数绑定为响应式数据,并且深拷贝一份用于重置,避免重置时因为引用关系导致数据没清干净的bug。
4.5 数据统计看板的ECharts集成
老板和店长最关心的数据可视化,我用ECharts实现。热词里有人在搜“vue element-plus 实现 echarts 箱线图boxplot 多个箱子”,那类高级图表这里用不上,但ECharts和Vue3的集成方法是一样的。关键是安装依赖后,在组件里正确初始化图表实例:
<script setup> import * as echarts from 'echarts' import { ref, onMounted, onBeforeUnmount } from 'vue' import { dealStatsApi } from '../api/stats' const chartRef = ref(null) let chart = null onMounted(async () => { const res = await dealStatsApi() chart = echarts.init(chartRef.value) chart.setOption({ title: { text: '本月成交趋势' }, tooltip: { trigger: 'axis' }, xAxis: { data: res.data.dates }, yAxis: {}, series: [{ type: 'line', data: res.data.counts, smooth: true, areaStyle: {} }] }) }) onBeforeUnmount(() => { chart.dispose() }) </script>有两个坑需要提一下:ECharts实例在组件卸载前一定要调用dispose()销毁,否则页面切换之后会报“There is a chart instance already initialized on the dom”的警告,严重的会导致内存泄露;图表容器必须有明确高度,否则初始化之后是一片空白。这两个问题我调试后总结成了团队内部文档,新同事照着做就不会踩同样的坑。
5. 核心业务模块的完整实现流程
5.1 房源发布与审核流转
业务员提交房源时,表单校验通过后,房源状态为“待审核”。店长登录后进入“房源审核”菜单,能看到所有待审核的房源,点击通过或者驳回。驳回时店长必须填写驳回理由,这个理由会通过列表里的提示组件显示给业务员看。
审核状态机的流转逻辑我画在代码里,用状态常量加条件判断实现:
const STATUS_PENDING = 0; // 待审核 const STATUS_APPROVED = 1; // 已通过 const STATUS_REJECTED = 2; // 已驳回 if ($house->status !== 0) { throw new HttpException(400, '该房源已处理不能重复审核'); }这个校验逻辑非常重要。如果后端不做这个状态判断,就可能出现运营人员在页面同时打开多个窗口,对同一套房源点了两次审核通过,数据库里状态变成乱套的情况。加了这个判断,不管前端怎么点,后端都只接受第一次操作。
5.2 客户跟进与带看流程闭环
客户管理模块的亮点在跟进记录的设计。业务员每联系一次客户,都要在跟进模块里新增一条记录,包含联系方式和沟通内容。系统会自动记录操作人和时间,并支持设置下次跟进时间。当天没有完成跟进记录的客户,会出现在首页看板的“待跟进提醒”里。
我带团队做需求分析时发现,业务员普遍反感强制填写跟进记录,觉得是“浪费时间”。后来在系统里加了一个功能:把跟进记录和统计排行关联起来,跟进次数和成交率挂钩,店长开会的时候能直观看到每个人的工作量。有了数据驱动,业务员才真正愿意用这个系统。
带看记录的流程是:业务员先选择客户、选择待看房源、预约带看时间,确认带看后填写看房反馈,包括客户满意程度、价格意向、竞品对比信息。反馈信息推送到该房源所属的店长工作台,辅助店长判断是否需要与业主沟通议价。
5.3 成交管理与佣金计算
成交登记是整个交易链路的终点。核心控制点有两个:房源和客户的状态变更,以及佣金计算。
成交登记时,首先要校验房源状态是“在售”,然后把房源状态改为“已售”,把客户状态改为“已成交”。这两个操作必须在一个数据库事务里执行,避免出现房源已售但客户还是跟进中的数据不一致。ThinkPHP的数据库事务写法:
Db::transaction(function () use ($data) { $house = House::find($data['house_id']); if ($house->status !== 1) { throw new \Exception('房源状态异常,无法成交'); } $house->status = 3; $house->save(); $deal = new DealRecord(); $deal->house_id = $data['house_id']; $deal->customer_id = $data['customer_id']; $deal->deal_price = $data['deal_price']; $deal->commission_rate = $data['commission_rate']; $deal->commission_amount = round($data['deal_price'] * $data['commission_rate'] / 100, 2); $deal->save(); });佣金计算这里有个细节,佣金比例是按百分比存的,比如1.5表示1.5%,计算佣金时要除以100,最后保留两位小数。如果佣金比例是跟房价阶梯挂钩的,还需要在系统里配置多级费率表。我第一次实现时忽略了这个点,直接写死一个比例,运营人员后来提出不同等级的房源佣金不一样,需求变更起来就是噩梦。所以业务系统里凡是关联“规则”的字段,尽量做成可配置,而不是写死在代码里。
5.4 统计报表模块
统计报表包含三个维度:房源维度(各区域房源数、价格分布)、客户维度(客户来源、意向等级分布)、成交维度(月度成交趋势、佣金排名)。
报表接口统一返回原始统计数据,前端ECharts图表展示。举一个SQL统计的例子,按区域统计在售房源数量:
$list = House::field('district, count(id) as total') ->where('status', 1) ->group('district') ->select();这种聚合查询在数据量大时必须建好索引,我在这张表的district、status字段上都加了普通索引。数据量到几十万条的时候,有没有索引查询速度差几十倍。
6. 部署上线与常见问题排查
6.1 前后端打包部署方案
开发完成之后要上线,这里分享一下生产环境的部署经验。
前端打包:
npm run build构建完成后会生成dist目录,里面是纯静态文件。我一般用Nginx直接托管这些静态文件,然后配置反向代理,把/api开头的请求转发到后端的PHP服务。Nginx的关键配置如下:
server { listen 80; server_name house.example.com; # 前端静态文件 root /var/www/house/dist; index index.html; # 处理Vue Router的history模式,刷新404问题 location / { try_files $uri $uri/ /index.html; } # API反向代理到ThinkPHP location /api/ { proxy_pass http://127.0.0.1:8000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }热词里有人在搜“nginx部署前端vue项目”和“vue项目启动后network不可用”,第一个问题参考上面配置就能解决,第二个问题通常是开发环境监听地址的问题,Vite默认只监听localhost,要局域网访问得在server.host配置0.0.0.0。
后端部署的坑更多一些。ThinkPHP在Nginx环境下的伪静态配置必须正确,否则访问二级路由会出现404。Nginx的配置:
location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } }另外PostgreSQL和MySQL的大小写敏感规则不一样,如果迁移过数据库要特别小心。PHP环境里proc_open、shell_exec这类函数如果没特殊需求,建议在php.ini的disable_functions里禁掉,防止恶意命令执行。
6.2 开发中高频问题的排查方法
做一个实战类的管理系统,总会遇到不少坑。我把自己开发过程中遇到的高频问题整理成一张速查表:
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 前端登录发请求,Network显示红字 | 后端跨域未允许 | 检查CORS配置或Vite代理 |
| 列表页刷新出现404 | Vue Router history模式没配置 | Nginx里加try_files |
| 表单提交显示401 | Token过期或未注入 | 检查请求头,重新登录 |
| 中文数据在页面乱码 | 数据库字符集不是utf8mb4 | 修改数据表和连接的CHARSET |
| 文件上传提示“非法文件” | PHPfileinfo扩展没开启 | 在php.ini里启用扩展 |
| 接口报500但日志没有记录 | PHP错误被隐藏 | 打开APP_DEBUG,看实时错误 |
| 前端改了代码页面不变 | 热更新失效或浏览器缓存 | 重启DevServer,强刷浏览器 |
其中“Token过期后页面异常”是我在系统上线之后遇到的最大问题。用户挂在系统里不动,8小时Token到期后,用户点任何按钮都会收到401错误,但前端响应拦截器里的逻辑是弹提示并跳登录页。一开始跳转的效果不稳定,有时候登录页弹出来是白屏。排查发现是路由守卫和资源加载的时序问题:401触发跳转时如果对应的路由组件在懒加载中尚未返回,页面会短暂空白。解决方案是在跳转前先清空整个Vue黑名单缓存,强制重新挂载根组件,问题就消失了。
6.3 系统的性能优化与后续演进
系统跑了大半年,几千套房源和上万条跟进记录,整体响应时间还能控制在200ms以内,主要得益于几点:第一,核心数据表的查询索引建得比较全;第二,后端做了查询缓存,热门的筛选条件结果缓存60秒;第三,前端表格全部分页,不做一次性全量加载;第四,静态资源和图片走CDN加速,减轻服务器压力。
后续演进方向有很多:小程序端是刚需,客户在外边看房的时候随时能用手机查房源;房产的图片和VR看房可以接入对象的存储服务和播放器;智能推荐方面,根据客户带看历史推荐相似房源。热词里有人搜“vue播放m3u8”,说明不少同行都在折腾房源视频和VR展示,这块也是房产系统的标配功能。
7. 结合开发经验的一点想法
做这套系统最大的收获,是明白了一个道理:技术框架选型永远不是项目成功的决定因素,对业务场景的理解才是最花钱的部分。ThinkPHP和Vue都是非常成熟的技术,网上教程一抓一大把,但真正让系统值钱的,是你能不能把房源状态、客户跟进、成交佣金这套业务逻辑设计得清晰可靠,能不能用代码把线下手工流程的漏洞堵住。
热词里有人搜“thinkphp 3.2 版本兼容php8”,这个我特别想多说一句。3.2是非常老的版本了(应该是2014年左右的东西),如果还在维护老项目,建议尽早规划迁移。PHP 8带来的性能提升和语法改进是革命性的,继续停留在老版本生态里,安全漏洞和依赖兼容问题会越来越难处理。我当时就是从ThinkPHP 5.1平滑迁移到6.0的,主要工作是数据库查询构造器的写法调整和验证器重构,前后花了大概一周时间。
最后再分享一个实用的小技巧:开发这类前后端分离的管理系统,接口设计要尽量语义化,POST /api/house表示新增房源,GET /api/house?id=1表示获取房源详情,PUT /api/house?id=1表示更新房源,DELETE /api/house?id=1表示删除房源。这种RESTful风格虽然看似简单,但坚持用下来,前后端联调的时候可以省掉大量沟通成本。我的经验就是:后端定义好接口契约文档,前端按契约开发,整个项目的协作效率能提升50%以上。
如果你也准备做一个类似的房产销售管理系统,先别急着写代码,花几天时间把业务梳理清楚,跟真实用户聊聊他们的工作流程,画清楚状态机流转图,再动手搭建工程。这套思路无论用什么技术栈,都值得先走一遍。