居美家房产中介平台 - 项目计划文档
📌 项目概述
项目名称
居美家房产中介平台
客户方
居美家房产中介公司
项目背景
为居美家房产中介公司开发一套房源租售管理系统,支持微信小程序和移动H5端,方便经纪人发布房源,用户浏览查找房源,管理员统一管理平台数据。
项目范围
- 服务区域: 青岛市北区
- 小程序类型: 个人小程序
- 服务器: 阿里云服务器 + 宝塔面板
- 腾讯地图密钥: PVZBZ-DB46Z-WFEXI-ZCKQA-HZBWS-BLBF2
项目目标
- 提供便捷的房源发布和管理功能
- 为用户提供优质的房源浏览和搜索体验
- 实现高效的房源信息展示
- 降低运营成本,提高工作效率
设计参考
- 房源信息参考:安居客房源详情页
- UI设计风格:安居客APP(现代简约,符合中国人使用习惯)
🏗️ 技术架构
前端技术栈
- 框架: uni-app (主要适配微信小程序,兼容H5)
- 核心库: Vue 3
- UI组件库: uni-ui
- 地图服务: 腾讯地图 API
- 状态管理: Pinia
- 网络请求: uni.request 封装
后端技术栈
- 框架: ThinkPHP 8.0
- 语言: PHP 8.1+
- 架构模式: MVC (Model-View-Controller)
- ORM: ThinkPHP ORM (支持链式操作)
- 数据库: MySQL 5.7
- 缓存: Redis (支持多种缓存驱动)
- Web服务器: Nginx + PHP-FPM
- 文件存储: 本地文件系统
- 依赖管理: Composer
- 特性:
- 容器管理和依赖注入
- 中间件机制
- 验证器
- ORM模型关联
- RESTful路由
开发模式
- 个人开发
- 快速迭代
- 无预算约束(使用免费/开源方案)
👥 用户角色
1. 普通用户
- 浏览房源列表
- 查看房源详情
- 搜索房源
- 地图找房
- 联系经纪人
2. 经纪人
- 登录账号
- 发布房源(无需审核,直接上线)
- 管理自己的房源
- 编辑/删除/上下架房源
- 申请添加小区
3. 管理员
- 最高权限
- 查看所有房源
- 用户管理(审核经纪人申请)
- 小区管理(审核小区申请)
- 数据统计
🎯 核心功能模块
模块一:用户模块
1.1 用户认证
- 小程序端: 微信授权登录
- H5端: 邮箱验证码登录(QQ邮箱SMTP)
- 自动获取用户基本信息
邮箱验证码规则
- 验证码有效期: 5分钟
- 同一邮箱发送间隔: 60秒
1.2 角色管理
- 默认注册为普通用户
- 用户申请成为经纪人(任何人均可申请)
- 管理员后台简单审核(信息登记审核,无需上传资质证明)
经纪人申请字段
- 真实姓名(必填)
- 联系电话(必填)
- 所属公司(必填)
- 备注说明(选填)
1.3 个人中心
- 用户信息展示
- 我的房源(经纪人)
- 浏览历史
- 记录用户浏览过的房源
- 未登录用户通过JWT中的anonymous_id标识
- 登录用户关联到账号(user_id)
- 登录时自动迁移匿名浏览历史到用户账号
- 统一使用JWT Token维护用户身份状态
- 设置与退出
模块二:房源模块
2.1 房源列表(首页)
展示方式
- 瀑布流/卡片式布局
- 缩略图 + 关键信息
- Tab页切换(二手房/租房)
内容信息
- 房源图片(首图)
- 标题
- 价格(租金/售价)
- 面积、户型
- 小区名称
- 楼层信息
- 房源标签
- 经纪人信息
交互功能
- 下拉刷新
- 上拉加载更多
- Tab分类切换(二手房/租房)
- 排序选项:
- 最新发布(默认)
- 价格升序/降序
- 面积大小
- 浏览量排序(热门房源)
2.2 房源详情
图片视频区
- 轮播图展示(支持图片、视频)
- 支持全屏预览
- 图片计数显示
基本信息
- 价格(大字突出显示)
- 面积
- 户型(X室X厅X卫)
- 楼层(X/共X层)
- 朝向
- 装修情况
- 房源标签(经纪人手动选择,支持多选和新增)
小区信息
- 小区名称
- 详细地址
- 建筑年代
- 物业费
- 配套设施(自动获取周边:地铁站、学校、医院、商场等)
地图位置
- 嵌入腾讯地图
- 显示小区位置
- 周边设施标注
房源描述
- 经纪人填写的文字描述
经纪人信息
- 头像、昵称
- 在售房源数量
操作按钮
- 联系经纪人(点击后拨打用户表中的手机号)
- 分享房源(支持多种方式):
- 分享到微信好友
- 分享到朋友圈
- 生成海报图片
- 复制链接
2.3 发布房源
Step 1: 选择小区
- 搜索小区名称
- 显示候选列表
- 若找不到,可申请添加小区
Step 2: 房源类型
- 二手房 / 租房
Step 3: 上传媒体
- 上传房源图片(最多9张,单张最大5MB)
- 上传视频(1个,可选,最大100MB,时长不限)
- 设置封面图
Step 4: 填写信息
- 标题
- 价格
- 面积
- 户型(室、厅、卫)
- 楼层 / 总楼层
- 朝向(东、南、西、北、南北、东西等)
- 装修情况(毛坯、简装、精装、豪装)
- 房源标签(多选,可新增)
- 房源描述
Step 5: 提交发布
- 直接发布,无需审核
- 自动上架
2.4 我的房源
- 已发布房源列表
- 房源状态标识(上架中/已下架)
- 快捷操作:编辑、删除、上/下架
- 数据统计展示:
- 总房源数
- 上架中数量
- 已下架数量
- 总浏览量
重要说明
- 房源永久有效,除非手动下架或删除
- 已下架房源:
- 普通用户可查看但标注下架状态
- 不会出现在搜索结果中
- 不会出现在列表筛选中
模块三:小区模块
3.1 小区数据来源
数据积累策略
- 完全依赖经纪人申请和管理员添加逐步积累
- 使用腾讯地图POI搜索获取小区信息
- 自动获取:名称、地址、经纬度、周边配套
- 管理员手动补充:建筑年代、物业费等
- 初始数据库为空,随使用逐步完善
3.2 小区管理(管理员)
添加小区
- 输入小区名称
- 调用腾讯地图API搜索(限定青岛市北区)
- 选择正确的POI结果
- 自动填充:
- 小区名称
- 详细地址
- 经纬度
- 手动填写:
- 建筑年代
- 物业费
- 备注信息
周边配套自动获取
- 添加小区时调用腾讯地图周边搜索API获取一次
- 存储到数据库,定期更新(管理员手动触发或定时任务)
- 获取附近500米内:
- 地铁站
- 公交站
- 学校
- 医院
- 商场/超市
- 银行
- 存储为JSON格式
小区列表
- 查看所有小区
- 编辑小区信息
- 删除小区(需检查是否有关联房源)
3.3 小区申请(经纪人)
- 经纪人发布房源时,若小区不存在
- 填写小区申请表单:
- 小区名称(必填)
- 详细地址(必填)
- 小区照片(必填,至少1张)
- 建筑年代(必填)
- 物业费(必填)
- 备注说明(选填)
- 提交后等待管理员审核
- 审核通过后可继续发布房源
模块四:搜索模块
4.1 关键词搜索
- 搜索框(支持小区名)
- 直接搜索小区名称,显示该小区的所有房源
- 搜索历史记录
- 热门搜索推荐(可选)
- 搜索结果列表
4.2 条件筛选
筛选条件
- 房源类型:二手房/租房
- 价格区间:自定义/预设区间
- 面积区间:自定义/预设区间
- 户型:1室、2室、3室、4室、5室+
- 小区筛选:输入小区名称搜索
- 朝向:东、南、西、北、南北、东西
- 装修:毛坯、简装、精装、豪装
- 房源标签:多选标签筛选
筛选交互
- 筛选条件浮层
- 已选条件标签展示
- 清空筛选
- 确认筛选
模块五:管理后台
5.1 数据统计(首页)
实时统计数据
- 房源总数
- 在售房源数
- 用户总数
- 经纪人数量
今日数据
- 今日新增房源
- 今日新增用户
- 今日浏览量
周期对比
- 本周数据对比(与上周)
- 本月数据对比(与上月)
待处理事项
- 待审核经纪人申请
- 待审核小区申请
5.2 房源管理
- 查看所有房源列表
- 筛选条件(状态、类型、经纪人、时间)
- 查看房源详情
- 编辑房源(管理员可编辑所有房源)
- 删除违规房源
- 批量操作(可选)
5.3 用户管理
用户列表
- 所有用户列表
- 用户角色筛选
- 查看用户详情
- 禁用/启用用户
经纪人审核
- 待审核经纪人列表
- 查看申请信息
- 通过/拒绝申请
5.4 小区管理
小区列表
- 所有小区列表
- 搜索小区
- 编辑小区信息
- 删除小区
小区申请审核
- 待审核小区申请
- 查看申请详情
- 通过/拒绝申请
- 通过后自动创建小区
5.5 标签管理
- 房源标签列表
- 新增标签
- 编辑标签
- 删除标签(检查是否被房源使用)
- 标签统一管理,方便后续搜索和筛选
💾 数据库设计
表结构设计
1. 用户表 (users)
CREATE TABLE `users` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`openid` VARCHAR(100) COMMENT '微信openid(小程序)',
`email` VARCHAR(100) COMMENT '邮箱(H5)',
`password` VARCHAR(255) COMMENT '密码(H5邮箱登录用)',
`phone` VARCHAR(20) COMMENT '手机号',
`nickname` VARCHAR(50) COMMENT '昵称',
`avatar` VARCHAR(255) COMMENT '头像',
`role` ENUM('user', 'agent', 'admin') DEFAULT 'user' COMMENT '角色',
`status` TINYINT DEFAULT 1 COMMENT '状态 1:正常 0:禁用',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `idx_openid` (`openid`),
UNIQUE KEY `idx_email` (`email`),
KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';2. 房源表 (properties)
CREATE TABLE `properties` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`property_no` VARCHAR(20) NOT NULL COMMENT '房源编号 如:FY202510290001',
`user_id` INT UNSIGNED NOT NULL COMMENT '发布者ID',
`community_id` INT UNSIGNED NOT NULL COMMENT '小区ID',
`title` VARCHAR(100) NOT NULL COMMENT '标题',
`type` ENUM('sale', 'rent') NOT NULL COMMENT '类型 sale:二手房 rent:租房',
`price` DECIMAL(10,2) NOT NULL COMMENT '价格(元/月)',
`area` DECIMAL(6,2) NOT NULL COMMENT '面积(㎡)',
`room` TINYINT NOT NULL COMMENT '室',
`hall` TINYINT NOT NULL COMMENT '厅',
`toilet` TINYINT NOT NULL COMMENT '卫',
`floor` TINYINT NOT NULL COMMENT '楼层',
`total_floor` TINYINT NOT NULL COMMENT '总楼层',
`orientation` VARCHAR(20) COMMENT '朝向',
`decoration` VARCHAR(20) COMMENT '装修情况',
`tags` VARCHAR(500) COMMENT '标签(逗号分隔)',
`images` TEXT COMMENT '图片(JSON数组)',
`thumbnail` VARCHAR(255) COMMENT '缩略图(第一张图自动生成)',
`video` VARCHAR(255) COMMENT '视频',
`description` TEXT COMMENT '描述',
`views` INT DEFAULT 0 COMMENT '浏览次数',
`status` TINYINT DEFAULT 1 COMMENT '状态 1:上架 0:下架',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `idx_property_no` (`property_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_community_id` (`community_id`),
KEY `idx_type` (`type`),
KEY `idx_status` (`status`),
KEY `idx_create_time` (`create_time`),
KEY `idx_views` (`views`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房源表';3. 小区表 (communities)
CREATE TABLE `communities` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(100) NOT NULL COMMENT '小区名称',
`address` VARCHAR(255) NOT NULL COMMENT '详细地址(青岛市北区)',
`lat` DECIMAL(10,7) NOT NULL COMMENT '纬度',
`lng` DECIMAL(10,7) NOT NULL COMMENT '经度',
`build_year` VARCHAR(10) COMMENT '建筑年代',
`property_fee` VARCHAR(50) COMMENT '物业费',
`facilities` JSON COMMENT '周边配套设施',
`status` TINYINT DEFAULT 1 COMMENT '状态 1:正常 0:禁用',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小区表(青岛市北区)';4. 小区申请表 (community_applications)
CREATE TABLE `community_applications` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` INT UNSIGNED NOT NULL COMMENT '申请人ID',
`name` VARCHAR(100) NOT NULL COMMENT '小区名称',
`address` VARCHAR(255) NOT NULL COMMENT '地址',
`images` TEXT COMMENT '小区照片(JSON数组)',
`build_year` VARCHAR(10) NOT NULL COMMENT '建筑年代',
`property_fee` VARCHAR(50) NOT NULL COMMENT '物业费',
`lat` DECIMAL(10,7) COMMENT '纬度',
`lng` DECIMAL(10,7) COMMENT '经度',
`remark` VARCHAR(255) COMMENT '备注',
`status` ENUM('pending', 'approved', 'rejected') DEFAULT 'pending' COMMENT '状态',
`reject_reason` VARCHAR(255) COMMENT '拒绝原因',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小区申请表';5. 经纪人申请表 (agent_applications)
CREATE TABLE `agent_applications` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` INT UNSIGNED NOT NULL COMMENT '申请人ID',
`real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名',
`phone` VARCHAR(20) NOT NULL COMMENT '联系电话',
`company` VARCHAR(100) NOT NULL COMMENT '所属公司(必填)',
`remark` VARCHAR(255) COMMENT '备注',
`status` ENUM('pending', 'approved', 'rejected') DEFAULT 'pending' COMMENT '状态',
`reject_reason` VARCHAR(255) COMMENT '拒绝原因',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='经纪人申请表';6. 标签表 (tags)
CREATE TABLE `tags` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50) NOT NULL COMMENT '标签名称',
`sort` INT DEFAULT 0 COMMENT '排序',
`status` TINYINT DEFAULT 1 COMMENT '状态 1:启用 0:禁用',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='房源标签表';7. 邮箱验证码表 (email_codes)
CREATE TABLE `email_codes` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`email` VARCHAR(100) NOT NULL COMMENT '邮箱',
`code` VARCHAR(10) NOT NULL COMMENT '验证码',
`expire_time` TIMESTAMP NOT NULL COMMENT '过期时间',
`is_used` TINYINT DEFAULT 0 COMMENT '是否已使用 1:已使用 0:未使用',
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
KEY `idx_email` (`email`),
KEY `idx_expire_time` (`expire_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邮箱验证码表';8. 浏览历史表 (view_history)
CREATE TABLE `view_history` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` INT UNSIGNED DEFAULT NULL COMMENT '用户ID(登录用户)',
`anonymous_id` VARCHAR(100) DEFAULT NULL COMMENT '匿名ID(未登录用户,从JWT中提取)',
`property_id` INT UNSIGNED NOT NULL COMMENT '房源ID',
`view_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '浏览时间',
KEY `idx_user_id` (`user_id`),
KEY `idx_anonymous_id` (`anonymous_id`),
KEY `idx_property_id` (`property_id`),
KEY `idx_view_time` (`view_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='浏览历史表';🔌 接口设计 (RESTful API)
API 基础信息
- Base URL:
https://api.fangyou.com/v1 - 认证方式: JWT Token(统一认证体系)
- 请求格式: JSON
- 响应格式: JSON
JWT 认证策略
Token 类型
1. 匿名 Token(未登录用户)
- 首次访问公开接口时自动分配
- 有效期:30天
- 用于追踪匿名用户浏览历史
{
"type": "anonymous",
"anonymous_id": "anon_20251029123456_6720abc123_a1b2c3d4e5f6",
"iat": 1698566400,
"exp": 1701158400
}2. 用户 Token(已登录用户)
- 登录成功后生成
- 有效期:7天
- 包含用户身份和权限信息
{
"type": "user",
"user_id": 123,
"role": "agent",
"email": "user@example.com",
"iat": 1698566400,
"exp": 1699171200
}接口认证规则
| 接口类型 | 认证要求 | Token 类型 | 说明 |
|---|---|---|---|
| 公开接口 | 可选 | anonymous/user | 无 Token 时自动分配匿名 Token |
| 需登录接口 | 必须 | user | 匿名 Token 会返回 401 |
| 管理员接口 | 必须 | user + admin 角色 | 需验证角色权限 |
Token 传递方式
GET /api/properties/123
Authorization: Bearer {token}自动 Token 分配机制
- 客户端首次请求公开接口时,请求头可不带 Token
- 服务端检测到无有效 Token,自动生成匿名 Token
- 响应中返回新 Token,客户端保存后用于后续请求
登录后数据迁移
- 用户登录时,服务端自动将匿名 Token 对应的浏览历史迁移到用户账号
- 返回新的用户 Token,客户端替换原匿名 Token
统一响应格式
{
"code": 200,
"message": "success",
"data": {},
"token": "新Token(仅在首次访问或Token更新时返回)"
}接口列表
1. 用户模块 (/api/auth)
1.1 微信登录
POST /api/auth/wx-login
参数: { code: string }
返回: { token: string, userInfo: object }1.2 邮箱验证码登录(H5)
POST /api/auth/email-login
参数: { email: string, code: string }
返回: { token: string, userInfo: object }1.3 发送邮箱验证码
POST /api/auth/send-email-code
参数: { email: string }
返回: { success: boolean, message: string }
限制: 同一邮箱60秒内只能发送一次1.4 获取当前 Token 信息
GET /api/auth/token-info
Headers: Authorization: Bearer {token}
返回: {
type: string (anonymous/user),
user_id: number (仅user类型),
anonymous_id: string (仅anonymous类型),
role: string (仅user类型)
}1.5 获取用户信息
GET /api/user/info
Headers: Authorization: Bearer {token}
认证要求: 必须登录(user token)
返回: { userInfo: object }1.6 申请成为经纪人
POST /api/user/apply-agent
参数: {
real_name: string (必填),
phone: string (必填),
company: string (必填),
remark: string (选填)
}
返回: { application_id: number }2. 房源模块 (/api/properties)
2.1 房源列表
GET /api/properties
参数:
- page: number (页码)
- pageSize: number (每页数量)
- type: string (sale/rent)
- sort: string (latest/price_asc/price_desc/area)
返回: { list: array, total: number, page: number }2.2 房源详情
GET /api/properties/:id
认证要求: 可选(无token时自动分配匿名token)
返回: {
property: object,
token: string (首次访问时返回新的匿名token)
}
说明: 自动记录浏览历史(已登录用户关联user_id,未登录用户关联anonymous_id)2.3 发布房源
POST /api/properties
认证要求: 必须登录(user token)
参数: {
community_id, title, type, price, area,
room, hall, toilet, floor, total_floor,
orientation, decoration, images, video, description
}
返回: { property_id: number }2.4 编辑房源
PUT /api/properties/:id
认证要求: 必须登录(user token)
参数: { ... 同发布房源 }
返回: { success: boolean }2.5 删除房源
DELETE /api/properties/:id
认证要求: 必须登录(user token)
返回: { success: boolean }2.6 上下架房源
PUT /api/properties/:id/status
认证要求: 必须登录(user token)
参数: { status: number }
返回: { success: boolean }2.7 我的房源
GET /api/my/properties
认证要求: 必须登录(user token)
参数:
- page: number
- pageSize: number
- status: number (可选)
返回: { list: array, total: number }2.8 浏览历史
GET /api/my/view-history
认证要求: 可选(支持匿名和登录用户)
返回: {
list: array (最近50条浏览记录),
total: number
}
说明:
- 已登录用户: 返回user_id关联的浏览历史
- 匿名用户: 返回anonymous_id关联的浏览历史
- 登录后会自动迁移匿名浏览历史3. 小区模块 (/api/communities)
3.1 小区列表
GET /api/communities
参数:
- page: number
- pageSize: number
返回: { list: array, total: number }3.2 搜索小区
GET /api/communities/search
参数: { keyword: string }
返回: { list: array }3.3 申请添加小区
POST /api/communities/apply
认证要求: 必须登录(user token)
参数: {
name: string (必填),
address: string (必填),
images: array (必填,小区照片),
build_year: string (必填),
property_fee: string (必填),
lat: number (选填),
lng: number (选填),
remark: string (选填)
}
返回: { application_id: number }3.4 腾讯地图POI搜索
GET /api/map/poi
参数: { keyword: string }
说明: 默认限定青岛市北区范围
返回: { list: array }4. 搜索模块 (/api/search)
4.1 综合搜索
GET /api/search
参数: { keyword: string, page, pageSize }
返回: { list: array, total: number }4.2 条件筛选
POST /api/search/filter
参数: {
type: string,
price_min: number,
price_max: number,
area_min: number,
area_max: number,
room: array,
district: string,
orientation: string,
decoration: string,
page: number,
pageSize: number
}
返回: { list: array, total: number }5. 上传模块 (/api/upload)
5.1 上传图片
POST /api/upload/image
认证要求: 必须登录(user token)
Content-Type: multipart/form-data
参数: { file: File }
返回: { url: string }5.2 上传视频
POST /api/upload/video
认证要求: 必须登录(user token)
Content-Type: multipart/form-data
参数: { file: File }
返回: { url: string }6. 管理后台 (/api/admin)
所有管理后台接口均需要:认证要求: 必须登录(user token) + admin 角色
6.1 数据统计
GET /api/admin/stats
返回: {
// 实时统计
total_properties: number,
online_properties: number,
total_users: number,
total_agents: number,
// 今日数据
today_properties: number,
today_users: number,
today_views: number,
// 周期对比
week_comparison: {
properties: { current: number, last: number, change: number },
users: { current: number, last: number, change: number },
views: { current: number, last: number, change: number }
},
month_comparison: {
properties: { current: number, last: number, change: number },
users: { current: number, last: number, change: number },
views: { current: number, last: number, change: number }
},
// 待处理
pending_agents: number,
pending_communities: number
}6.2 所有房源
GET /api/admin/properties
参数: { page, pageSize, status, type, user_id, start_time, end_time }
返回: { list: array, total: number }6.3 用户列表
GET /api/admin/users
参数: { page, pageSize, role, status }
返回: { list: array, total: number }6.4 审核经纪人
PUT /api/admin/agents/:id
参数: { status: 'approved'|'rejected', reject_reason }
返回: { success: boolean }6.5 小区管理
GET /api/admin/communities
POST /api/admin/communities
PUT /api/admin/communities/:id
DELETE /api/admin/communities/:id6.6 审核小区申请
GET /api/admin/community-applications
PUT /api/admin/community-applications/:id
参数: { status: 'approved'|'rejected', reject_reason }6.7 标签管理
GET /api/admin/tags # 标签列表
POST /api/admin/tags # 新增标签
PUT /api/admin/tags/:id # 编辑标签
DELETE /api/admin/tags/:id # 删除标签📁 项目目录结构
fangyou/
│
├── client/ # uni-app 前端项目
│ ├── pages/
│ │ ├── index/ # 首页(房源列表)
│ │ │ ├── index.vue
│ │ │ └── components/
│ │ │ ├── PropertyCard.vue
│ │ │ └── FilterBar.vue
│ │ │
│ │ ├── property/ # 房源详情
│ │ │ └── detail.vue
│ │ │
│ │ ├── publish/ # 发布房源
│ │ │ ├── index.vue
│ │ │ ├── select-community.vue
│ │ │ └── form.vue
│ │ │
│ │ ├── search/ # 搜索
│ │ │ ├── index.vue
│ │ │ └── filter.vue
│ │ │
│ │ ├── map/ # 地图找房
│ │ │ └── index.vue
│ │ │
│ │ ├── my/ # 个人中心
│ │ │ ├── index.vue
│ │ │ ├── properties.vue # 我的房源
│ │ │ ├── apply-agent.vue # 申请经纪人
│ │ │ └── settings.vue
│ │ │
│ │ ├── auth/ # 登录
│ │ │ ├── login.vue
│ │ │ └── phone-login.vue
│ │ │
│ │ └── admin/ # 管理后台
│ │ ├── index.vue # 数据统计
│ │ ├── properties.vue # 房源管理
│ │ ├── users.vue # 用户管理
│ │ ├── agents.vue # 经纪人审核
│ │ ├── communities.vue # 小区管理
│ │ └── community-apply.vue # 小区申请审核
│ │
│ ├── components/ # 全局组件
│ │ ├── ImageUpload.vue # 图片上传
│ │ ├── VideoUpload.vue # 视频上传
│ │ ├── CommunitySelector.vue # 小区选择器
│ │ ├── MapSelector.vue # 地图选点
│ │ └── Empty.vue # 空状态
│ │
│ ├── api/ # API封装
│ │ ├── request.js # 请求封装
│ │ ├── auth.js # 用户相关
│ │ ├── property.js # 房源相关
│ │ ├── community.js # 小区相关
│ │ ├── search.js # 搜索相关
│ │ ├── upload.js # 上传相关
│ │ └── admin.js # 管理后台
│ │
│ ├── utils/ # 工具函数
│ │ ├── auth.js # 登录鉴权
│ │ ├── validator.js # 表单验证
│ │ ├── format.js # 格式化
│ │ ├── map.js # 地图工具
│ │ └── constants.js # 常量配置
│ │
│ ├── store/ # 状态管理 (Pinia)
│ │ ├── index.js
│ │ ├── user.js # 用户状态
│ │ ├── property.js # 房源状态
│ │ └── app.js # 应用状态
│ │
│ ├── static/ # 静态资源
│ │ ├── images/
│ │ └── icons/
│ │
│ ├── uni_modules/ # uni-ui组件
│ │
│ ├── App.vue # 应用入口
│ ├── main.js # 入口文件
│ ├── manifest.json # 应用配置
│ ├── pages.json # 页面配置
│ └── uni.scss # 全局样式
│
├── server/ # ThinkPHP 8 后端项目
│ ├── app/
│ │ ├── controller/ # 控制器
│ │ │ ├── Auth.php # 用户认证
│ │ │ ├── Property.php # 房源管理
│ │ │ ├── Community.php # 小区管理
│ │ │ ├── Search.php # 搜索
│ │ │ ├── Upload.php # 文件上传
│ │ │ ├── Tag.php # 标签管理
│ │ │ ├── ViewHistory.php # 浏览历史
│ │ │ └── Admin.php # 管理后台
│ │ │
│ │ ├── model/ # 模型
│ │ │ ├── User.php
│ │ │ ├── Property.php
│ │ │ ├── Community.php
│ │ │ ├── CommunityApplication.php
│ │ │ ├── AgentApplication.php
│ │ │ ├── Tag.php
│ │ │ ├── EmailCode.php
│ │ │ └── ViewHistory.php
│ │ │
│ │ ├── service/ # 业务逻辑层
│ │ │ ├── AuthService.php
│ │ │ ├── JwtService.php # JWT Token生成与解析(支持匿名/用户Token)
│ │ │ ├── PropertyService.php
│ │ │ ├── CommunityService.php
│ │ │ ├── MapService.php # 腾讯地图API
│ │ │ ├── EmailService.php # 邮件服务(QQ邮箱SMTP)
│ │ │ ├── ImageService.php # 图片处理(缩略图生成)
│ │ │ └── UploadService.php
│ │ │
│ │ ├── middleware/ # 中间件
│ │ │ ├── Auth.php # JWT认证(必须用户Token)
│ │ │ ├── OptionalAuth.php # 可选认证(自动分配匿名Token)
│ │ │ ├── Role.php # 角色权限
│ │ │ └── Cors.php # 跨域
│ │ │
│ │ ├── validate/ # 验证器
│ │ │ ├── PropertyValidate.php
│ │ │ ├── CommunityValidate.php
│ │ │ └── UserValidate.php
│ │ │
│ │ ├── common.php # 公共函数
│ │ ├── event.php # 事件定义
│ │ └── provider.php # 服务提供
│ │
│ ├── config/ # 配置文件
│ │ ├── app.php # 应用配置
│ │ ├── database.php # 数据库配置
│ │ ├── cache.php # 缓存配置(Redis)
│ │ ├── filesystem.php # 文件系统
│ │ ├── jwt.php # JWT配置(自定义)
│ │ ├── map.php # 腾讯地图配置(自定义)
│ │ ├── email.php # 邮件配置(自定义,QQ邮箱SMTP)
│ │ └── route.php # 路由配置
│ │
│ ├── route/ # 路由目录
│ │ ├── api.php # API路由
│ │ └── app.php # 应用路由
│ │
│ ├── public/ # 公开目录(web根目录)
│ │ ├── index.php # 入口文件
│ │ ├── .htaccess # Apache重写规则
│ │ ├── robots.txt
│ │ └── uploads/ # 上传文件存储
│ │ ├── images/
│ │ └── videos/
│ │
│ ├── runtime/ # 运行时目录
│ │ ├── cache/ # 缓存目录
│ │ ├── log/ # 日志目录
│ │ └── temp/ # 临时目录
│ │
│ ├── vendor/ # Composer依赖
│ │
│ ├── think # 命令行工具
│ ├── composer.json # Composer配置
│ ├── .env # 环境变量
│ └── .example.env # 环境变量示例
│
├── database/ # 数据库
│ ├── migrations/ # 迁移文件
│ │ └── init.sql # 初始化SQL
│ └── seeds/ # 种子数据
│ └── admin_seed.sql # 管理员账号
│
├── docs/ # 文档
│ ├── api.md # API文档
│ ├── database.md # 数据库设计文档
│ └── deployment.md # 部署文档
│
├── .gitignore
├── README.md
└── 项目计划.md # 本文档🔧 ThinkPHP 8 技术要点
安装与初始化
# 使用Composer创建ThinkPHP 8项目
composer create-project topthink/think server
# 安装ThinkPHP8扩展包
cd server
composer require topthink/think-orm
composer require topthink/think-cache
composer require topthink/think-filesystem
composer require firebase/php-jwt # JWT认证
composer require phpmailer/phpmailer # 邮件发送(QQ邮箱SMTP)
composer require intervention/image # 图片处理(缩略图生成)核心特性应用
1. 路由定义 (route/api.php)
use think\facade\Route;
// RESTful 资源路由
Route::resource('properties', 'Property');
// 自定义路由组
Route::group('api', function() {
// 用户认证
Route::post('auth/login', 'Auth/login');
Route::post('auth/register', 'Auth/register');
// 房源相关 (需要认证)
Route::group(function() {
Route::get('properties', 'Property/index');
Route::get('properties/:id', 'Property/read');
Route::post('properties', 'Property/save');
Route::put('properties/:id', 'Property/update');
Route::delete('properties/:id', 'Property/delete');
})->middleware(['Auth']);
// 管理后台 (需要管理员权限)
Route::group('admin', function() {
Route::get('stats', 'Admin/stats');
Route::get('properties', 'Admin/properties');
})->middleware(['Auth', 'Role:admin']);
});2. 模型定义 (app/model/Property.php)
namespace app\model;
use think\Model;
class Property extends Model
{
// 表名
protected $name = 'properties';
// 自动时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
// JSON字段
protected $json = ['images', 'facilities'];
// 类型转换
protected $type = [
'price' => 'float',
'area' => 'float',
'status' => 'integer',
];
// 关联小区
public function community()
{
return $this->belongsTo(Community::class);
}
// 关联用户
public function user()
{
return $this->belongsTo(User::class);
}
// 搜索器:类型
public function searchTypeAttr($query, $value)
{
if ($value) {
$query->where('type', $value);
}
}
// 搜索器:价格区间
public function searchPriceAttr($query, $value)
{
if (isset($value[0]) && isset($value[1])) {
$query->whereBetween('price', $value);
}
}
}3. 控制器 (app/controller/Property.php)
namespace app\controller;
use app\BaseController;
use app\model\Property as PropertyModel;
use app\validate\PropertyValidate;
use think\Request;
class Property extends BaseController
{
// 房源列表
public function index(Request $request)
{
$page = $request->param('page', 1);
$pageSize = $request->param('pageSize', 10);
$list = PropertyModel::with(['community', 'user'])
->withSearch(['type', 'price'], $request->param())
->where('status', 1)
->order('create_time desc')
->paginate([
'list_rows' => $pageSize,
'page' => $page
]);
return json([
'code' => 200,
'message' => 'success',
'data' => $list
]);
}
// 房源详情
public function read($id)
{
$property = PropertyModel::with(['community', 'user'])
->find($id);
if (!$property) {
return json(['code' => 404, 'message' => '房源不存在']);
}
// 浏览量+1
$property->inc('views')->update();
return json([
'code' => 200,
'data' => $property
]);
}
// 发布房源
public function save(Request $request)
{
$data = $request->post();
// 验证
$validate = new PropertyValidate();
if (!$validate->check($data)) {
return json(['code' => 400, 'message' => $validate->getError()]);
}
// 获取当前用户ID
$data['user_id'] = $request->userId;
$property = PropertyModel::create($data);
return json([
'code' => 200,
'message' => '发布成功',
'data' => ['property_id' => $property->id]
]);
}
}4. 验证器 (app/validate/PropertyValidate.php)
namespace app\validate;
use think\Validate;
class PropertyValidate extends Validate
{
protected $rule = [
'title' => 'require|max:100',
'type' => 'require|in:sale,rent',
'price' => 'require|float|>:0',
'area' => 'require|float|>:0',
'room' => 'require|integer|>=:1',
'hall' => 'require|integer|>=:0',
'community_id' => 'require|integer',
];
protected $message = [
'title.require' => '请填写房源标题',
'title.max' => '标题最多100个字符',
'type.require' => '请选择房源类型',
'type.in' => '房源类型错误',
'price.require' => '请填写价格',
'price.float' => '价格格式错误',
];
}5. 中间件 (app/middleware/Auth.php)
namespace app\middleware;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class Auth
{
public function handle($request, \Closure $next)
{
$token = $request->header('authorization');
if (!$token) {
return json(['code' => 401, 'message' => '未登录']);
}
// 去除 Bearer 前缀
$token = str_replace('Bearer ', '', $token);
try {
$key = config('jwt.key');
$decoded = JWT::decode($token, new Key($key, 'HS256'));
// 将用户ID注入到request中
$request->userId = $decoded->user_id;
} catch (\Exception $e) {
return json(['code' => 401, 'message' => 'Token无效']);
}
return $next($request);
}
}6. 服务层 (app/service/MapService.php)
namespace app\service;
use think\facade\Cache;
class MapService
{
private $key;
public function __construct()
{
$this->key = config('map.tencent_key');
}
// POI搜索
public function searchPoi($keyword, $city = '')
{
$url = "https://apis.map.qq.com/ws/place/v1/search";
$params = [
'keyword' => $keyword,
'boundary' => "region({$city},0)",
'key' => $this->key,
];
// 缓存5分钟
$cacheKey = 'map_poi_' . md5($keyword . $city);
return Cache::remember($cacheKey, function() use ($url, $params) {
$response = file_get_contents($url . '?' . http_build_query($params));
return json_decode($response, true);
}, 300);
}
}配置文件
config/database.php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'type' => 'mysql',
'hostname' => env('database.hostname', '127.0.0.1'),
'database' => env('database.database', 'fangyou'),
'username' => env('database.username', 'root'),
'password' => env('database.password', ''),
'hostport' => env('database.hostport', '3306'),
'charset' => 'utf8mb4',
'prefix' => '',
],
],
];config/cache.php (Redis)
return [
'default' => 'redis',
'stores' => [
'redis' => [
'type' => 'redis',
'host' => env('redis.host', '127.0.0.1'),
'port' => env('redis.port', 6379),
'password' => env('redis.password', ''),
'select' => 0,
'timeout' => 0,
'expire' => 0,
'persistent' => false,
'prefix' => 'fangyou:',
],
],
];config/jwt.php (自定义)
return [
'key' => env('jwt.key', 'your-secret-key'),
'user_ttl' => 86400 * 7, // 用户Token有效期: 7天
'anonymous_ttl' => 86400 * 30, // 匿名Token有效期: 30天
'alg' => 'HS256',
];config/map.php (自定义)
return [
'tencent_key' => env('map.tencent_key', ''),
'default_city' => '青岛',
'default_district' => '市北区',
];config/email.php (自定义)
return [
'host' => env('email.host', 'smtp.qq.com'),
'port' => env('email.port', 587),
'username' => env('email.username', ''),
'password' => env('email.password', ''), // QQ邮箱授权码
'from_email' => env('email.from_email', ''),
'from_name' => env('email.from_name', '居美家'),
'code_expire' => 300, // 验证码有效期(秒) - 5分钟
'send_interval' => 60, // 发送间隔(秒) - 60秒
];.env 环境配置
APP_DEBUG = true
[DATABASE]
HOSTNAME = 127.0.0.1
DATABASE = fangyou
USERNAME = root
PASSWORD =
HOSTPORT = 3306
[REDIS]
HOST = 127.0.0.1
PORT = 6379
PASSWORD =
[JWT]
KEY = your-secret-key-change-in-production
[MAP]
TENCENT_KEY = PVZBZ-DB46Z-WFEXI-ZCKQA-HZBWS-BLBF2
[EMAIL]
HOST = smtp.qq.com
PORT = 587
USERNAME = your-qq-email@qq.com
PASSWORD = your-qq-authorization-code
FROM_EMAIL = your-qq-email@qq.com
FROM_NAME = 居美家常用命令
# 启动开发服务器
php think run
# 清除缓存
php think clear
# 生成控制器
php think make:controller Property
# 生成模型
php think make:model Property
# 生成中间件
php think make:middleware Auth
# 生成验证器
php think make:validate Property📅 开发计划
Phase 1: 项目初始化(1-2天)
- [x] 项目需求整理
- [ ] 项目结构搭建
- [ ] 开发环境配置
- [ ] 数据库设计与创建
- [ ] Git仓库初始化
Phase 2: 核心功能开发(7-10天)
Week 1: 基础模块
Day 1-2: 用户模块
- [ ] 微信登录(小程序)
- [ ] 邮箱验证码登录(H5)
- [ ] QQ邮箱SMTP服务集成
- [ ] JWT认证中间件
- [ ] 用户信息管理
Day 3-5: 房源列表与详情
- [ ] 房源列表页面
- [ ] 房源详情页面
- [ ] 房源数据接口
- [ ] 图片上传功能
Day 6-7: 发布房源
- [ ] 小区选择功能
- [ ] 发布房源表单
- [ ] 图片/视频上传
- [ ] 发布房源接口
Week 2: 完善功能
Day 8-9: 小区管理
- [ ] 腾讯地图API集成
- [ ] POI搜索功能
- [ ] 小区管理(管理端)
- [ ] 小区申请流程
Day 10-11: 搜索功能
- [ ] 关键词搜索
- [ ] 条件筛选
- [ ] 搜索结果页
Day 12-13: 我的房源
- [ ] 我的房源列表
- [ ] 编辑房源
- [ ] 删除房源
- [ ] 上下架功能
Phase 3: 管理后台(3-4天)
Day 14-15: 管理功能
- [ ] 管理后台首页(数据统计)
- [ ] 房源管理
- [ ] 用户管理
- [ ] 经纪人审核
Day 16-17: 小区与标签管理
- [ ] 小区列表与编辑
- [ ] 小区申请审核
- [ ] 标签管理功能
- [ ] 权限控制完善
Phase 4: 进阶功能(3-5天)
Day 18: 浏览历史与分享功能
- [ ] 浏览历史记录功能(基于JWT匿名Token)
- [ ] 登录后数据迁移机制
- [ ] 房源分享功能(微信/海报/链接)
Day 19-20: 优化与完善
- [ ] 缩略图自动生成功能
- [ ] 房源编号生成逻辑
- [ ] 列表分页优化
- [ ] 缓存策略(Redis)
- [ ] 错误提示优化(Toast/Modal)
- [ ] 数据统计优化(周期对比)
Day 21: 初始数据准备
- [ ] 预设房源标签数据
- [ ] 预设装修类型选项
- [ ] 预设朝向选项
- [ ] 管理员账号数据
Day 22: 测试与修复
- [ ] 功能测试
- [ ] 浏览历史测试
- [ ] 分享功能测试
- [ ] Bug修复
- [ ] 性能优化
Phase 5: 部署上线(1-2天)
Day 23-24: 部署
- [ ] 服务器环境配置
- [ ] 代码部署
- [ ] 小程序提审
- [ ] H5域名配置
- [ ] 数据备份方案
🎨 UI设计要点
设计风格
- 现代简约风格
- 参考安居客APP
- 符合中国用户使用习惯
- 清晰的信息层级
色彩方案
- 主色调:推荐蓝色系(#1890FF)或绿色系(#52C41A)
- 辅助色:灰色系
- 强调色:橙红色(价格等重要信息)
组件规范
- 遵循uni-ui设计规范
- 统一的间距、圆角、阴影
- 良好的点击反馈
- 友好的空状态
🔒 安全性考虑
1. 用户认证
- JWT Token认证
- Token过期机制
- 刷新Token策略
2. 数据安全
- SQL注入防护(预处理语句)
- XSS攻击防护
- 敏感信息加密存储
- 手机号脱敏显示
3. 接口安全
- 接口鉴权
- 请求频率限制
- 图片上传类型/大小限制
- 防止恶意上传
4. 权限控制
- 用户只能操作自己的房源
- 管理员权限验证
- 角色权限中间件
⚡ 性能优化
1. 前端优化
- 图片懒加载
- 列表虚拟滚动(长列表)
- 路由懒加载
- 静态资源CDN
2. 后端优化
- 数据库索引优化
- Redis缓存热门数据
- 图片压缩与缩略图
- 分页查询
3. 图片优化
- 房源第一张图自动生成缩略图
- 缩略图用于列表展示,提高加载速度
- 原图用于详情页查看
- 暂不做图片压缩和水印处理
📊 数据统计
已实现统计
- 房源浏览量(每个房源)
- 今日新增房源
- 今日新增用户
- 今日浏览量
- 本周/本月数据对比
- 经纪人房源数统计
进阶统计(后期扩展)
- 用户行为分析
- 热门房源排行
- 搜索热词
- 转化率统计
- 经纪人业绩排名
🚀 后期扩展功能
Phase N+1: 增值功能
- [ ] 房源推广(置顶、精选)
- [ ] VIP经纪人
- [ ] 在线咨询/聊天
- [ ] 预约看房
- [ ] 房源收藏功能
- [ ] 房源比对
- [ ] VR看房
- [ ] 消息通知(看房提醒)
- [ ] 地图找房功能
Phase N+2: 运营功能
- [ ] 分享裂变
- [ ] 优惠券系统
- [ ] 积分系统
- [ ] 推荐算法
- [ ] SEO优化
📝 注意事项
开发注意
- 服务范围:仅针对青岛市北区
- 不需要审核机制:经纪人发布房源直接上线
- 小区数据积累:完全依赖经纪人申请和管理员添加
- 无需支付功能:免费使用模式
- 个人小程序:功能受限但满足需求
- 本地存储文件:图片和视频存储在服务器本地
- 文件大小限制:
- 图片单张最大5MB
- 视频最大100MB,时长不限
- 邮箱登录:H5端使用邮箱验证码登录(QQ邮箱SMTP)
- 管理员权限:可编辑所有房源
- 房源编号:自动生成格式如 FY202510290001
- 缩略图生成:上传房源图片时,第一张图自动生成缩略图
- 浏览历史与JWT认证:
- 统一使用JWT Token认证体系
- 未登录用户自动分配匿名Token(anonymous_id)
- 登录用户使用用户Token(user_id)
- 登录时自动迁移匿名浏览历史
- 房源有效期:永久有效,除非手动下架或删除
- 下架房源:用户可查看但不可搜索
- 错误提示:优先使用Toast轻提示,根据场景选择Modal
- 初始数据:预设房源标签、装修类型、朝向等基础数据
- JWT认证架构:
- 去除Session机制,统一使用JWT
- 公开接口支持匿名访问(自动分配临时Token)
- 需登录接口验证用户Token
- 管理员接口验证Token + 角色
- 无状态设计,易于扩展
- ThinkPHP特性使用:
- 充分利用ORM链式操作
- 使用验证器进行数据验证
- 使用中间件进行权限控制(Auth/OptionalAuth/Role)
- 使用模型关联简化查询
- 使用Service层处理JWT逻辑
测试要点
- 小程序各机型适配(个人小程序限制)
- H5不同浏览器兼容
- 邮箱验证码发送与验证测试
- 图片/视频上传限制测试(5MB/100MB)
- 缩略图自动生成测试
- 房源编号生成测试
- 标签管理功能测试
- JWT认证机制测试:
- 匿名Token自动分配测试
- 用户Token登录认证测试
- Token过期处理测试
- 登录后数据迁移测试(匿名→用户)
- 浏览历史测试:
- 未登录用户浏览记录(anonymous_id)
- 登录用户浏览记录(user_id)
- 登录后历史记录合并测试
- 分享功能测试(微信/海报/链接)
- 数据统计测试(周期对比)
- 下架房源显示逻辑测试
- 大数据量性能测试
- 权限控制测试:
- 公开接口(OptionalAuth中间件)
- 需登录接口(Auth中间件)
- 管理员接口(Role中间件)
- 阿里云+宝塔环境部署测试
运维要点
- 定期数据备份
- 日志记录与监控
- 服务器资源监控
- 图片存储空间管理
📞 联系方式
项目负责人
- 开发者:[张江]
- 联系方式:[待填写]
客户方
- 公司名称:居美家房产中介公司
- 联系人:[待填写]
- 联系方式:[待填写]
📄 附录
相关链接
- 安居客房源详情参考:https://qd.anjuke.com/prop/view/S3814196878188556
- uni-app官方文档:https://uniapp.dcloud.net.cn/
- uni-ui组件库:https://uniapp.dcloud.net.cn/component/uniui/uni-ui.html
- ThinkPHP 8.0官方文档:https://doc.thinkphp.cn/v8_0/
- 腾讯地图API:https://lbs.qq.com/
- JWT for PHP:https://github.com/firebase/php-jwt
版本历史
- v1.0.0 (2025-10-29):初始版本,完成项目计划
- v1.1.0 (2025-10-29):需求细化,明确所有功能点和技术细节
- v1.2.0 (2025-10-29):完善所有细节需求,补充数据统计、图片处理、浏览历史等功能
- v1.3.0 (2025-10-29):重大更新:采用统一JWT认证方案,去除Session机制
- 实现匿名Token自动分配机制
- 支持登录后数据自动迁移
- 新增OptionalAuth中间件
- 完善JWT认证架构设计文档
📋 需求确认总结
以下是与客户确认的所有关键信息:
✅ 第一轮确认事项
- 服务区域:青岛市北区
- 小区数据:完全依赖经纪人申请和管理员添加逐步积累
- 经纪人审核:只需简单的信息登记审核,无需上传资质证明
- 文件限制:
- 图片:单张最大5MB
- 视频:最大100MB,时长无限制
- H5登录:邮箱验证码登录(QQ邮箱SMTP),验证码有效期5分钟,发送间隔60秒
- 管理员账号:直接在数据库插入
- 房源编辑权限:管理员可以编辑所有房源,经纪人只能编辑自己的
- 服务器:阿里云服务器 + 宝塔面板
- 预计用户数:青岛市北区人数规模
- 腾讯地图密钥:PVZBZ-DB46Z-WFEXI-ZCKQA-HZBWS-BLBF2
- 房源状态:目前只有"上架/下架"两种状态
- 小程序类型:个人小程序
- 区域筛选:通过小区名称搜索筛选
- 经纪人申请:所属公司必填,任何人都可申请成为经纪人
- 房源标签:
- 经纪人手动选择,可多选
- 支持新增标签
- 后台统一管理标签
- 标签可用于搜索和筛选
- 联系方式:点击"联系"按钮后弹出拨号,显示用户表中的手机号
- 数据库名称:fangyou
- 图片存储:server/public/uploads/images/
- 我的房源统计:展示总房源数、上架数、下架数、总浏览量
- 开发方式:uni-app同步开发小程序和H5,功能完全一致
- 首页展示:Tab页切换二手房/租房,默认排序为最新发布
✅ 第二轮补充确认
- 数据统计:
- 今日新增房源、今日新增用户、今日浏览量
- 本周/本月数据对比
- 分享功能:支持微信好友、朋友圈、生成海报、复制链接
- 排序方式:最新发布(默认)、价格升降序、面积、浏览量(热门)
- 周边配套:添加小区时获取一次存入数据库,定期更新
- 图片处理:
- 暂不做压缩、水印处理
- 房源第一张图自动生成缩略图
- 列表展示使用缩略图
- 浏览历史(已更新为JWT方案):
- 未登录用户通过JWT中的anonymous_id标识
- 登录用户关联到账号(user_id)
- 登录时自动迁移匿名浏览历史
- 统一使用JWT Token维护用户身份状态
- 下架房源:
- 普通用户可查看但标注下架状态
- 不在搜索结果和列表筛选中出现
- 小区申请:需提供小区照片、建筑年代、物业费等
- 初始数据:预设房源标签、装修类型、朝向等基础数据
- 房源有效期:永久有效,除非手动下架或删除
- 地图找房:暂不开发,列入后期扩展
- 错误提示:优先Toast轻提示,根据场景选择Modal
- 数据导出:暂不需要
- 房源编号:需要编号,格式如 FY202510290001
- 小区表简化:移除省市区字段,项目只针对青岛市北区
✅ 第三轮架构优化确认
- JWT统一认证方案:
- 去除Session机制,统一使用JWT Token
- 未登录用户自动分配匿名Token(30天有效期)
- 登录用户使用用户Token(7天有效期)
- 公开接口使用OptionalAuth中间件(自动分配匿名Token)
- 需登录接口使用Auth中间件(必须用户Token)
- 管理员接口使用Auth+Role中间件
- 数据迁移机制:登录时自动将匿名Token关联的浏览历史迁移到用户账号
- 浏览历史存储:数据库字段从session_id改为anonymous_id
- 无状态架构:服务端无需存储Session,易于水平扩展
- 前端Token管理:统一管理一个Token,自动保存服务端返回的新Token
🔐 JWT 统一认证方案详细设计
方案概述
本项目采用统一 JWT 认证体系,去除传统 Session 机制,实现无状态认证架构。所有用户(包括未登录的匿名用户)均使用 JWT Token 标识身份。
Token 类型设计
1. 匿名 Token(Anonymous Token)
用途:未登录用户的临时身份标识
生成时机:
- 用户首次访问公开接口时
- 没有携带 Token 或 Token 无效时
Payload 结构:
{
"type": "anonymous",
"anonymous_id": "anon_20251029123456_6720abc123_a1b2c3d4e5f6",
"iat": 1698566400,
"exp": 1701158400
}特性:
- 有效期:30 天
- 自动分配,无需用户操作
- 用于追踪匿名用户行为(浏览历史等)
- 登录后自动失效,数据迁移到用户账号
2. 用户 Token(User Token)
用途:已登录用户的身份认证
生成时机:
- 用户登录成功后
- Token 刷新时
Payload 结构:
{
"type": "user",
"user_id": 123,
"role": "agent",
"email": "user@example.com",
"iat": 1698566400,
"exp": 1699171200
}特性:
- 有效期:7 天
- 包含用户 ID、角色等关键信息
- 支持权限验证
- 退出登录时客户端删除即可
中间件设计
1. OptionalAuth(可选认证中间件)
适用场景:公开接口,支持匿名和登录用户访问
工作流程:
1. 检查请求头中的 Token
├─ 有 Token
│ ├─ 解析成功 → 注入用户信息到 request
│ └─ 解析失败 → 生成新的匿名 Token
└─ 无 Token → 生成新的匿名 Token
2. 注入信息到 request:
- userId (已登录) 或 anonymousId (匿名)
- tokenType ('user' 或 'anonymous')
- newToken (如果生成了新 Token)
3. 继续执行业务逻辑应用接口:
- 房源列表
- 房源详情
- 搜索接口
- 浏览历史
2. Auth(强制认证中间件)
适用场景:需要登录的接口
工作流程:
1. 检查请求头中的 Token
└─ 无 Token → 返回 401 未登录
2. 解析 Token
└─ 解析失败 → 返回 401 Token无效
3. 验证 Token 类型
├─ type === 'user' → 通过验证
└─ type === 'anonymous' → 返回 401 请先登录
4. 注入用户信息到 request:
- userId
- userRole
- tokenType
5. 继续执行业务逻辑应用接口:
- 发布房源
- 编辑房源
- 我的房源
- 申请经纪人
- 文件上传
3. Role(角色权限中间件)
适用场景:管理员接口
前置要求:必须先经过 Auth 中间件
工作流程:
1. 从 request 获取 userRole
└─ 不存在 → 返回 401 未登录
2. 验证角色权限
├─ role === 'admin' → 通过验证
└─ role !== 'admin' → 返回 403 无权限
3. 继续执行业务逻辑应用接口:
- 所有
/api/admin/*接口
核心服务实现
JwtService.php
namespace app\service;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtService
{
/**
* 生成匿名 Token
*/
public function generateAnonymousToken()
{
$payload = [
'type' => 'anonymous',
'anonymous_id' => $this->generateAnonymousId(),
'iat' => time(),
'exp' => time() + config('jwt.anonymous_ttl'),
];
return JWT::encode($payload, config('jwt.key'), 'HS256');
}
/**
* 生成用户 Token
*/
public function generateUserToken($user)
{
$payload = [
'type' => 'user',
'user_id' => $user->id,
'role' => $user->role,
'email' => $user->email,
'iat' => time(),
'exp' => time() + config('jwt.user_ttl'),
];
return JWT::encode($payload, config('jwt.key'), 'HS256');
}
/**
* 解析 Token
*/
public function decode($token)
{
try {
return JWT::decode($token, new Key(config('jwt.key'), 'HS256'));
} catch (\Exception $e) {
return null;
}
}
}数据迁移机制
登录时自动迁移匿名数据
场景:用户从匿名状态登录后,需要将之前的浏览历史等数据关联到用户账号
实现:
// AuthController::login()
public function login()
{
// 1. 验证登录信息...
$user = User::where('email', $email)->find();
// 2. 生成用户 Token
$token = $this->jwtService->generateUserToken($user);
// 3. 迁移匿名数据
$oldToken = $this->getToken();
if ($oldToken) {
$payload = $this->jwtService->decode($oldToken);
if ($payload && $payload->type === 'anonymous') {
// 迁移浏览历史
ViewHistory::where('anonymous_id', $payload->anonymous_id)
->where('user_id', null)
->update([
'user_id' => $user->id,
'anonymous_id' => null
]);
}
}
// 4. 返回新 Token
return json([
'code' => 200,
'data' => [
'token' => $token,
'userInfo' => $user
]
]);
}前端集成方案
Token 管理(utils/auth.js)
const TOKEN_KEY = 'token';
export function getToken() {
return uni.getStorageSync(TOKEN_KEY) || '';
}
export function setToken(token) {
if (token) {
uni.setStorageSync(TOKEN_KEY, token);
}
}
export function removeToken() {
uni.removeStorageSync(TOKEN_KEY);
}请求拦截(api/request.js)
import { getToken, setToken, removeToken } from '@/utils/auth';
function request(options) {
const token = getToken();
return new Promise((resolve, reject) => {
uni.request({
...options,
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...options.header
},
success: (res) => {
// 自动保存服务端返回的新 Token(匿名 Token)
if (res.data.token) {
setToken(res.data.token);
}
if (res.data.code === 200) {
resolve(res.data);
} else if (res.data.code === 401) {
// Token 过期或无效
removeToken();
// 可选:跳转到登录页
reject(res.data);
} else {
reject(res.data);
}
},
fail: reject
});
});
}登录流程
// 登录
export function login(email, code) {
return request({
url: '/api/auth/email-login',
method: 'POST',
data: { email, code }
}).then(res => {
// 保存用户 Token(覆盖匿名 Token)
setToken(res.data.token);
// 服务端已自动迁移匿名浏览历史
return res.data.userInfo;
});
}
// 退出登录
export function logout() {
removeToken();
// 下次访问公开接口时会自动获得新的匿名 Token
}路由配置示例
// route/api.php
use think\facade\Route;
// 公开接口(OptionalAuth 中间件)
Route::group('api', function() {
Route::get('properties', 'Property/index');
Route::get('properties/:id', 'Property/read');
Route::get('search', 'Search/index');
Route::get('my/view-history', 'Property/viewHistory');
})->middleware(['OptionalAuth']);
// 需登录接口(Auth 中间件)
Route::group('api', function() {
Route::post('properties', 'Property/save');
Route::put('properties/:id', 'Property/update');
Route::delete('properties/:id', 'Property/delete');
Route::get('my/properties', 'Property/myProperties');
})->middleware(['Auth']);
// 管理员接口(Auth + Role 中间件)
Route::group('api/admin', function() {
Route::get('stats', 'Admin/stats');
Route::get('properties', 'Admin/properties');
Route::get('users', 'Admin/users');
})->middleware(['Auth', 'Role:admin']);
// 认证接口(无需中间件)
Route::post('api/auth/email-login', 'Auth/emailLogin');
Route::post('api/auth/wx-login', 'Auth/wxLogin');方案优势
- 统一架构:所有认证逻辑统一使用 JWT,代码更简洁
- 无状态设计:服务端无需存储 Session,易于水平扩展
- 自动分配:匿名用户无感知获得 Token,提升体验
- 数据连续性:登录后自动迁移匿名数据,无缝衔接
- 灵活权限:通过中间件组合实现多层次权限控制
- 易于维护:前端只需管理一个 Token,逻辑清晰
安全考虑
- Token 签名:使用 HS256 算法签名,防止伪造
- 过期时间:
- 匿名 Token:30 天(较长,减少重新分配)
- 用户 Token:7 天(适中,兼顾安全和体验)
- HTTPS:生产环境必须使用 HTTPS 传输
- 密钥管理:JWT 密钥存储在环境变量中,不提交代码库
- Token 刷新:Token 过期后需要重新登录(简化版,后期可实现刷新机制)
文档更新日期:2025年10月29日