47 KiB
For AI assistants: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
Fonrey 房产经纪管理系统 — DATA MODEL 设计文档
作者: Backend Architect
版本: v1.4
日期: 2026-04-24(v1.1 修复 S1/S2/S4;v1.2 扩展 public schema;v1.3 §三 DDL 迁至 DATA_MODEL_PUBLIC.md,本文改为索引;v1.4 补充 LOGIN/PERMISSION 子文档引用、领域对象、租户 Schema 章节、Redis 缓存策略)
技术栈: Django 4.x + PostgreSQL + django-tenants + Redis
设计目标: 支撑 89,000+ 房源、多租户隔离、sub-100ms 查询、合规审计
一、架构决策总览 (Architecture Decision Records)
1.1 多租户策略:Schema-per-Tenant
┌──────────────────────────────────────────────────────────────────────┐
│ PostgreSQL Instance │
│ │
│ ┌─────────────────────────┐ ┌──────────────────┐ ┌────────────┐ │
│ │ public schema │ │ tenant_abc │ │ tenant_xyz │ │
│ │ (平台运营层) │ │ schema │ │ schema │ │
│ │ │ │ │ │ │ │
│ │ - tenants │ │ - org_units │ │ (同左) │ │
│ │ - domains │ │ - staff │ │ │ │
│ │ - tenant_status_logs │ │ - complexes │ │ │ │
│ │ - platform_admins │ │ - properties │ │ │ │
│ │ - admin_mfa_devices │ │ - clients │ │ │ │
│ │ - admin_sessions │ │ - user_accounts │ │ │ │
│ │ - ip_whitelist │ │ - login_attempts │ │ │ │
│ │ - platform_audit_logs │ │ - permission_defs│ │ │ │
│ │ - backup_schedules │ │ - roles │ │ │ │
│ │ - backup_records │ │ - staff_roles │ │ │ │
│ │ - export_tasks │ │ - lookup_items │ │ │ │
│ │ - system_versions │ │ - ... │ │ │ │
│ │ - upgrade_events │ └──────────────────┘ └────────────┘ │
│ │ - enum_labels │ │
│ └─────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
选型理由:
django-tenants的 Schema 隔离提供最强的数据安全边界- 房产经纪公司之间数据绝对不能互通(合规要求)
- 每个 Schema 独立索引,避免全局锁竞争
- 支持按租户独立备份/恢复
1.2 核心领域模型关系图
[区域/商圈]──────────────────────────────┐
│ │
[学校管理] │
│ ▼
[楼盘/小区] ──── [楼栋] ─────────► [房源] ◄──── [挂牌历史]
│ │
│ ┌────────┼────────┐
│ │ │ │
│ [联系人] [跟进日志] [维护完成度]
│ │ │
│ ┌─────┘ ┌────┴──────┐
│ │ │ │
│ [电话查看] [钥匙] [委托] [实勘]
│
[客源] ──── [配对记录] ──── [带看记录]
│
[员工/组织] ──── [权限]
1.3 关键设计原则
| 原则 | 决策 |
|---|---|
| 主键类型 | UUID v4(跨环境安全,避免枚举攻击) |
| 软删除 | 所有核心表含 deleted_at(历史可追溯) |
| 时间戳 | 全部使用 TIMESTAMPTZ(含时区) |
| 手机号存储 | AES-256-GCM 加密存储,建立 SHA-256 哈希索引 |
| 审计字段 | created_by, updated_by 全表覆盖 |
| 枚举值 | 业务枚举用 VARCHAR + CHECK,系统枚举用 lookup 表 |
| 大文本 | TEXT 类型,不设长度(PG 内部优化) |
| 金额 | NUMERIC(12,2) 万元精度,避免浮点误差 |
二、领域概览(Domain Overview)
本节用业务语言描述系统的核心领域对象及其关系,作为各子模块数据模型的导读。
核心领域对象
Public Schema(平台运营层)
| 领域对象 | 表 | 业务说明 |
|---|---|---|
| Tenant(租户) | public.tenants |
每家房产经纪公司一条记录,含状态机(creating/active/suspended/pending_delete/deleted)、套餐、联系人 |
| Domain(域名) | public.domains |
子域名↔租户映射,支持多域名绑定,子域名创建后不可修改 |
| TenantStatusLog | public.tenant_status_logs |
租户状态变更不可变审计(append-only) |
| PlatformAdmin | public.platform_admins |
平台管理员账号,3 种角色:超级管理员/运营人员/只读审计员 |
| AdminMfaDevice | public.admin_mfa_devices |
管理员 TOTP 设备(强制启用) |
| AdminSession | public.admin_sessions |
登录会话(30 分钟超时,支持强制登出) |
| IpWhitelist | public.ip_whitelist |
管理控制台 CIDR 白名单 |
| PlatformAuditLog | public.platform_audit_logs |
所有写操作+高危操作审计(append-only,建议月度分区) |
| BackupSchedule | public.backup_schedules |
全局/租户级定时备份计划(频率/保留数/存储目标) |
| BackupRecord | public.backup_records |
备份任务执行记录(自动/手动/升级前/恢复前) |
| ExportTask | public.export_tasks |
数据导出异步任务(CSV/JSON/SQL Dump,24h 下载链接) |
| SystemVersion | public.system_versions |
平台版本历史,唯一 current 版本约束 |
| UpgradeEvent | public.upgrade_events |
升级/回滚事件,含灰度租户维度进度快照 |
| EnumLabel | public.enum_labels |
固定枚举字典(英文 Key → 中文标签),所有租户共享,供前端下拉渲染、导出报表中文标签、日志快照使用 |
Tenant Schema(租户业务层)
| 领域对象 | 表/子文档 | 业务说明 |
|---|---|---|
| OrgUnit(组织架构) | org_units → DATA_MODEL_ORG.md |
树形组织架构(总部/区域/城市/大区/分公司/门店/团队/虚拟团队),物化路径存储,支持权限继承 |
| Staff(员工) | staff → DATA_MODEL_ORG.md |
经纪人/店长/经理,绑定组织节点,手机号加密存储,与账号(登录)分离 |
| District(城区) | districts → DATA_MODEL_COMPLEX.md |
行政区划,如「静安区」,是区域体系的顶层节点 |
| BusinessArea(商圈) | business_areas → DATA_MODEL_COMPLEX.md |
商圈/板块,从属于城区,一个楼盘可归属多个商圈 |
| School(学校) | schools → DATA_MODEL_COMPLEX.md |
对口学校数据库,是买家购房决策的核心参考,与楼盘多对多关联 |
| Complex(楼盘/小区) | complexes → DATA_MODEL_COMPLEX.md |
房源录入的基础底座,维护楼盘标准名称/坐标/锁定状态/别名等 |
| Building(楼栋/单元) | buildings → DATA_MODEL_COMPLEX.md |
楼盘下的物理楼栋,区分标准结构与非标结构 |
| RoomUnit(房号) | room_units → DATA_MODEL_COMPLEX.md |
楼层+房间号,房源定位的最细粒度 |
| Property(房源) | properties → DATA_MODEL_PROPERTY.md |
系统核心表,每套二手房源的完整档案,支持出售/出租/出售兼出租三态 |
| Client(客源) | clients → DATA_MODEL_CLIENT.md |
买家/租客档案,分私客/公客/成交客,含活跃度评分与自动公客转换机制 |
| Viewing(带看) | client_viewings → DATA_MODEL_CLIENT.md |
经纪人带客户看房的完整记录 |
| Match(配对) | client_property_matches → DATA_MODEL_CLIENT.md |
系统/人工推荐的客源↔房源配对 |
| UserAccount(用户账号) | user_accounts → DATA_MODEL_LOGIN.md |
系统登录主体,与员工档案 1:1 绑定,含账号锁定/密码历史/登录审计 |
| PermissionDef(权限定义) | permission_defs → DATA_MODEL_PERMISSION.md |
权限目录(约 300 条),驱动 Hybrid RBAC + Override 权限模型 |
| Role(业务角色) | roles → DATA_MODEL_PERMISSION.md |
权限模板,含 4 大类别(置业顾问/店管/总经/运营/自定义) |
领域关系快速导航
District (城区)
└─ BusinessArea (商圈)
└─ Complex (楼盘) ─── School (对口学校)
├─ Building (楼栋)
│ └─ RoomUnit (房号)
└─ Property (房源)
├─ PropertyContact (联系人/委托方)
├─ FollowLog (跟进日志)
├─ Viewing (带看记录) ──── Client (客源)
└─ Match (配对记录) ──────┘
OrgUnit (组织架构)
└─ Staff (员工/经纪人) ─── Property / Client / Viewing / Match
子文档索引
| 子文档 | 覆盖模块 | 状态 |
|---|---|---|
| DATA_MODEL_PUBLIC.md | Public schema 平台运营层(tenants, domains, platform_admins, admin_sessions, audit_logs, backup, export, upgrade 共 13 张表) | ✅ 完成 |
| DATA_MODEL_ORG.md | 组织人事(org_units, staff, 异动/奖惩/教育/家庭等) | ✅ 完成 |
| DATA_MODEL_COMPLEX.md | 楼盘/区域(districts, business_areas, complexes, buildings, room_units, schools 等) | ✅ 完成 |
| DATA_MODEL_CLIENT.md | 客源管理(clients, requirements, follow_logs, viewings, matches 等) | ✅ 完成 |
| DATA_MODEL_PROPERTY.md | 房源管理(properties 及配套 22 张表,含跟进/钥匙/委托/实勘/营销/产证/完成度/标签/收藏/保护/号码方审批等) | ✅ 完成 |
| DATA_MODEL_LOGIN.md | 登录与账号认证(user_accounts, login_attempts, password_reset_tokens, password_histories + Redis 登录缓存) | ✅ 完成 |
| DATA_MODEL_PERMISSION.md | 权限管理(permission_defs, roles, role_permissions, staff_roles, staff_permission_overrides, staff_data_scopes, permission_change_logs + Redis 权限缓存) | ✅ 完成 |
| ENUMS.md | 枚举字典(public.enum_labels 表设计 + 所有模块枚举定义 + 种子数据 SQL) |
✅ 完成 |
三、公共 Schema(Shared / Public)
权威源:完整 DDL 已迁至
DATA_MODEL_PUBLIC.md,本节仅保留摘要索引。
覆盖范围:publicschema 存储平台运营层数据——租户注册、管理员账号、审计日志、备份/导出任务、版本升级记录(共 13 张表)。
设计依据:系统管理模块 PRD(PRD/系统管理/系统管理模块PRD.md)。
表清单(开发以 DATA_MODEL_PUBLIC.md 为准)
| 表名 | 说明 | 节 |
|---|---|---|
public.tenants |
租户主表(django-tenants 核心,状态机 6 态) | §2.1 |
public.domains |
域名↔租户映射(支持多域名,子域名不可修改) | §2.1 |
public.tenant_status_logs |
租户状态变更不可变审计日志(append-only) | §2.1 |
public.platform_admins |
平台管理员账号(super_admin/ops_operator/read_only_auditor) | §2.2 |
public.admin_mfa_devices |
管理员 TOTP MFA 设备(强制启用) | §2.2 |
public.admin_sessions |
管理员登录会话(30 min 滚动超时,支持强制登出) | §2.2 |
public.ip_whitelist |
管理控制台 CIDR 白名单 | §2.2 |
public.platform_audit_logs |
所有写操作+高危操作审计(append-only,建议月度分区) | §2.3 |
public.backup_schedules |
全局/租户级定时备份计划(NULL tenant_id = 全局默认) | §2.4 |
public.backup_records |
备份任务执行记录(auto/manual/pre_upgrade/pre_restore) | §2.4 |
public.export_tasks |
数据导出异步任务(CSV/JSON/SQL Dump,24h 下载链接) | §2.4 |
public.system_versions |
平台版本历史,部分唯一索引保证唯一 current | §2.5 |
public.upgrade_events |
升级/回滚事件,tenant_progress JSONB 快照各租户状态 |
§2.5 |
public.enum_labels |
固定枚举字典(英文 Key → 中文标签),所有租户共享 | §2.6 |
关键约束提示:
tenant_status_logs/platform_audit_logs无 deleted_at,禁止 UPDATE/DELETE,append-onlypublic.tenants.schema_name创建后不可修改public.tenants不再使用is_activeboolean,改用 6 态status枚举platform_admins与租户staff完全独立,不共享 auth 系统system_versions通过部分唯一索引确保全局只有一个status='current'
四、租户 Schema(Tenant Schema)
以下所有表均在每个租户的独立 Schema 内创建。
3.1 组织人事模块(Organization & HR)
详细模型 → 见
DATA_MODEL_ORG.md
该文件为权威定义,包含完整字段、枚举、查询模式和禁止操作。
核心表概览(开发时以 DATA_MODEL_ORG.md 为准):
| 表名 | 说明 |
|---|---|
org_units |
组织树节点(公司/事业部/大区/区域/片区/门店/店组/职能),物化路径树 |
staff |
员工主表,含加密手机号、角色、在职状态、Django auth 绑定 |
staff_personal_info |
员工个人信息扩展(证件、学历、婚育等,1:1) |
staff_transfer_logs |
人事异动不可变审计日志(入职/调动/离职/复职等) |
staff_reward_punish |
奖惩记录 |
staff_work_experiences |
工作经历 |
staff_educations |
教育经历 |
staff_trainings |
培训经历 |
staff_family_members |
家庭成员 |
staff_accounts |
第三方平台账号绑定(58安居客/中国网络经纪人等) |
关键约束提示:
staff.phone_encAES-256-GCM 加密,staff.phone_hashSHA-256 用于唯一索引staff_transfer_logs无 deleted_at,不可删除org_units路径查询:WHERE path LIKE '/root/{target_id}/%'- 员工离职:
status = 'resigned'+deleted_at软删除,记录永久保留
3.2 区域与楼盘模块(Region & Complex Management)
详细模型 → 见
DATA_MODEL_COMPLEX.md
本节仅作概览,开发时以 DATA_MODEL_COMPLEX.md 为权威定义。
核心表概览(开发时以 DATA_MODEL_COMPLEX.md 为准):
| 表名 | 说明 | 关键字段 |
|---|---|---|
districts |
城区/行政区 | city, name, short_name, sort_order |
business_areas |
商圈/板块(从属于城区) | district_id, name, latitude, longitude |
metro_lines |
地铁线路 | city, name, color |
metro_stations |
地铁站点 | metro_line_id, name, latitude, longitude |
schools |
学校(对口学区) | district_id, name, type, nature, level |
complexes |
楼盘/小区(房源底座) | name, district_id, address, latitude/longitude, lock_*, search_vector |
complex_aliases |
楼盘别名(含系统别名/用户自定义别名) | complex_id, alias, is_system |
complex_business_areas |
楼盘↔商圈多对多(含主商圈标识) | complex_id, business_area_id, is_primary |
complex_schools |
楼盘↔学校关联(含学区类型) | complex_id, school_id, zone_type |
complex_metro_stations |
楼盘↔地铁站关联(含步行距离) | complex_id, station_id, distance_meters |
buildings |
楼栋/单元 | complex_id, name, is_standard, total_floors |
room_units |
房号/结构单元(楼层+房间号) | building_id, floor, room_no, is_standard |
complex_photos |
楼盘照片(楼盘图/户型图/VR) | complex_id, category, file_key, is_cover |
complex_attachments |
楼盘附件 | complex_id, file_key, file_name |
complex_price_trends |
楼盘价格走势(月度) | complex_id, record_month, avg_unit_price |
3.3 房源模块(Property Management)
详细模型 → 见
DATA_MODEL_PROPERTY.md
本节仅作概览,开发时以 DATA_MODEL_PROPERTY.md 为权威定义。
核心表概览(开发时以 DATA_MODEL_PROPERTY.md 为准):
| 表名 | 说明 | 关键字段 |
|---|---|---|
properties |
房源主表(系统核心,89,000+ 数据量) | status, attribute, property_type, complex_id, sale_price, area, grade, completeness_score, search_vector |
property_contacts |
业主/联系人(手机号 AES 加密+哈希索引) | property_id, phone_enc, phone_hash, identity, is_number_holder |
listing_histories |
挂牌历史快照(不可删除) | property_id, listing_type, status, sale_price, seller_agent_snapshot |
price_changes |
调价记录(不可删除) | property_id, old_sale_price, new_sale_price, change_reason, changed_by |
follow_logs |
跟进日志(6种类型,最高写入频率) | property_id, log_type, content, is_deletable, operator_id |
follow_log_attachments |
跟进附件(图片) | follow_log_id, file_key, file_type |
follow_log_recordings |
跟进录音 | follow_log_id, file_key, duration_seconds |
property_keys |
钥匙管理(机械钥匙/密码) | property_id, key_type, holder_id, is_active |
key_attachments |
钥匙附件 | key_id, file_key |
commissions |
委托管理(独家/非独家) | property_id, commission_type, period_start, status |
commission_attachments |
委托附件(身份证/产证/委托书) | commission_id, category, file_key |
field_surveys |
实勘管理(GPS 打卡) | property_id, status, gps_latitude, gps_longitude, created_by |
survey_photos |
实勘照片(按空间分类) | survey_id, category, file_key, is_vr_screenshot |
property_photos |
房源图片(经纪人管理,封面唯一约束) | property_id, category, is_cover, file_key |
property_attachments |
房源附件 | property_id, category, file_key |
property_marketing |
营销信息(1:1,卖点/业主心态/介绍) | property_id, marketing_title, core_selling_points |
property_certificates |
产证信息(1:1) | property_id, cert_no, owner_name, land_nature |
property_completeness |
维护完成度快照(1:1,Celery 异步计算) | property_id, total_score, score_survey, score_commission, ... |
property_tags |
标签字典(系统预置+运营自定义) | name, color, is_system |
property_tag_relations |
房源↔标签多对多 | property_id, tag_id |
property_favorites |
经纪人收藏房源 | staff_id, property_id |
property_protections |
保护房设置(1:1) | property_id, is_protected, start_at, end_at |
number_holder_approvals |
号码方变更审批 | property_id, applicant_id, status |
关键约束提示:
property_contacts.phone_hash是重复房源检测的主要依据,录入前必须查重listing_histories/price_changes无 deleted_at,不可删除follow_logs中is_deletable=FALSE(sensitive_view类型)不可软删completeness_score只由 Celery 任务写入,Application 层禁止直接更新last_followed_at由触发器trg_update_last_followed自动维护property_photos.is_cover唯一约束:每套房源仅一张封面
3.4 登录与账号认证(Login & Account)
详细模型 → 见
DATA_MODEL_LOGIN.md
该文件为权威定义,包含完整字段、状态机、Redis 缓存结构和禁止操作。
核心表概览(开发时以 DATA_MODEL_LOGIN.md 为准):
| 表名 | 说明 |
|---|---|
user_accounts |
账号主表(1:1 绑定 org.Staff),含加密手机号/哈希、状态机(active/locked/disabled)、初始密码标识 |
login_attempts |
登录审计日志(append-only,成功/失败均记录,无 FK 冗余存 username 保证历史完整) |
password_reset_tokens |
密码重置 Token(有效期 30 分钟,使用后立即标记 is_used) |
password_histories |
历史密码记录(保留最近 3 条,含初始密码,防止重复使用) |
关键约束提示:
user_accounts主键用BIGSERIAL(非 UUID),登录审计场景 BigInt 更高效user_accounts.phone_encAES-256-GCM 加密,phone_hashSHA-256 用于唯一索引- 禁止物理删除
user_accounts,离职员工只能status=disabled - 账号锁定(5 次密码错误)→
status=locked,locked_until=NOW()+30min;Redis 仅计数,实际锁定以 DB 为准 - Tenant Admin 的
staff_id可为空(可无员工档案);普通员工staff_id必填且关联 active Staff - 员工离职(
org.Staff.status→resigned)→ 应用层 Service 调用触发账号status→disabled,禁止循环 FK password_reset_tokens/login_attempts无 deleted_at,不可修改/删除
Redis 辅助状态(非持久化):
| Key 格式 | TTL | 说明 |
|---|---|---|
captcha_token:{uuid} |
3 分钟 | 滑块验证会话 Token |
captcha_pass:{uuid} |
3 分钟 | 一次性通过凭证(验证后立即删除) |
login_fail:{tenant_id}:{username} |
30 分钟 | 连续密码错误计数;≥5 触发锁定 |
recover_email:{email} |
1 小时 | 找回邮件发送次数上限 3 次 |
tenant_verify_ip:{ip} |
1 分钟 | Tenant 验证接口 IP 限流;≥10 次拒绝 |
3.5 权限管理(Permission & RBAC)
详细模型 → 见
DATA_MODEL_PERMISSION.md
该文件为权威定义,包含完整字段、权限解析算法、ScopeQueryBuilder实现和禁止操作。
权限模型概述:Hybrid RBAC + Individual Override,支持 BOOLEAN / SCOPE / INTEGER 三类权限值,多角色合并规则 OR / MAX。
核心表概览(开发时以 DATA_MODEL_PERMISSION.md 为准):
| 表名 | 说明 |
|---|---|
permission_defs |
权限目录(约 300 条,PUBLIC Schema 中 shared_apps 存储,所有租户共享),含模块/分组/值类型/默认值/上限类别 |
roles |
业务角色(每租户独立),5 种类别:agent/store_manager/director/operator/custom,含系统内置标识 |
role_permissions |
角色↔权限值(稀疏存储,仅存与 default_value 不同的项) |
staff_roles |
员工↔角色分配(N:M,含主角色标识 is_primary、有效期) |
staff_permission_overrides |
员工个人权限覆盖(稀疏存储,仅存与角色合并值不同的项),3 种 override_mode:REPLACE / RESTRICT / GRANT |
staff_data_scopes |
员工数据范围扩展(补充 SCOPE 权限之外的额外可读范围,如特殊跨门店授权) |
permission_change_logs |
权限变更不可变审计日志(append-only,禁止 UPDATE/DELETE) |
关键约束提示:
permission_defs位于 Public Schema(shared_apps),所有租户共享;roles及其余表属租户 Schema- 禁止硬删除
permission_defs,改用is_active=FALSE下线;code字段不可修改 - 禁止直接构造 Q 对象绕过
ScopeQueryBuilder,会导致越权漏洞 permission_change_logs无 deleted_at,禁止 UPDATE/DELETE- 员工权限解析:
is_system_admin=TRUE→ 短路返回全权限;否则多角色 OR/MAX 合并后叠加 Override StaffPermissionOverride保存前必须做差异对比,禁止存与角色合并值相同的冗余记录(稀疏存储)staff_roles.is_primary唯一约束通过 Signal 维护,禁止绕过
权限解析缓存:
| Cache Key | TTL | 失效触发 |
|---|---|---|
perm:v{VER}:{schema}:{staff_id} |
3600s | Override / StaffRole 变更 |
perm:v{VER}:{schema}:role:{role_id}:staff_ids |
3600s | 角色权限变更 → Pipeline 批量失效 |
perm:inconsistent:{schema}:{staff_id} |
300s | 同上 |
perm:defs:{schema} |
86400s | PermissionDef 变更(低频) |
perm:role_applied_count:{schema}:{role_id} |
600s | StaffRole 变更 |
版本号机制:
CACHE_VERSION在 Django settings 中,升级 PermissionDef 结构时 bump,一键全局失效。
3.17 客源管理(Client Management)
详细模型 → 见
DATA_MODEL_CLIENT.md
该文件为权威定义,包含完整字段、枚举、状态机、查询模式和禁止操作。
核心表概览(开发时以 DATA_MODEL_CLIENT.md 为准):
| 表名 | 说明 |
|---|---|
clients |
客源主表(私客/公客/成交客),含加密手机号哈希、活跃度、归属人 |
client_contacts |
联系人(1:N),手机号加密+哈希,支持多联系人 |
client_requirements |
需求信息(可多类型:二手/新房/租房),含预算/面积/商圈/朝向等偏好 |
client_follow_logs |
跟进日志(高写入频率,5种类型,敏感查看类型不可删) |
client_follow_log_attachments |
跟进附件(图片/录音,最大20MB) |
client_viewings |
带看/预约记录(1:N,含陪看人/合作带看人) |
client_property_matches |
智能配房结果(录客配房/系统配房,匹配度评分) |
client_status_logs |
状态变更不可变审计日志(改状态/改等级/转公/转成交/转无效等) |
client_favorite_folders |
私客收藏夹(经纪人自定义分组) |
client_folder_items |
收藏夹与客源的多对多关联 |
client_school_preferences |
意向学校(拆表,支持精确查询) |
关键约束提示:
client_contacts.phone_hash是重复客源检测的唯一依据,录入前必须查重client_status_logs无 deleted_at,不可删除- 私客超时(配置天数内无跟进)→ Celery 自动转公(
transfer_to_public_type = 'auto') - 活跃度
activity_level由 Celery 每日凌晨批量计算,不实时更新
3.18 系统设置(System Settings)
归属说明:
lookup_categories/lookup_items/saved_filters为跨模块系统表,权威定义在本节。property_tags/property_tag_relations/property_favorites/property_protections/number_holder_approvals属房源模块配套表,权威定义已迁至DATA_MODEL_PROPERTY.md §4.19-§4.22,本节不再重复 DDL(修复 S1/S2)。
-- ============================================================
-- 枚举/选项管理:跟进目的、标签、来源渠道 等运营维护的枚举值
-- ============================================================
CREATE TABLE lookup_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(50) UNIQUE NOT NULL, -- 如:follow_purpose, property_source
name VARCHAR(100) NOT NULL,
module VARCHAR(30) NOT NULL -- property/client/system
);
CREATE TABLE lookup_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
category_id UUID NOT NULL REFERENCES lookup_categories(id) ON DELETE CASCADE,
value VARCHAR(100) NOT NULL,
label VARCHAR(100) NOT NULL, -- 显示文本
sort_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
metadata JSONB NOT NULL DEFAULT '{}', -- 扩展属性
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_lookup_items_category ON lookup_items(category_id)
WHERE is_active = TRUE;
CREATE UNIQUE INDEX idx_lookup_items_value ON lookup_items(category_id, value);
-- 筛选方案(保存的搜索条件,跨模块通用)
CREATE TABLE saved_filters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
module VARCHAR(20) NOT NULL DEFAULT 'property',
filter_params JSONB NOT NULL, -- 完整筛选参数 JSON
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_saved_filters_staff ON saved_filters(staff_id, module);
已迁出本节的表(权威源见子文档):
| 表名 | 权威定义位置 |
|---|---|
property_tags |
DATA_MODEL_PROPERTY.md §4.19 |
property_tag_relations |
DATA_MODEL_PROPERTY.md §4.19 |
property_favorites |
DATA_MODEL_PROPERTY.md §4.20 |
property_protections |
DATA_MODEL_PROPERTY.md §4.21 |
number_holder_approvals |
DATA_MODEL_PROPERTY.md §4.22 |
3.19 枚举字典(Enum Labels)
权威定义 → 见
DATA_MODEL/ENUMS.md
本节为概览,开发时以 ENUMS.md 为准。
表归属
enum_labels 位于 Public Schema(shared_apps),所有租户共享,不属于任何租户 Schema。
核心表设计
CREATE TABLE enum_labels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
domain VARCHAR(60) NOT NULL, -- 枚举域,格式:{模块}.{字段},如 client.status
value VARCHAR(60) NOT NULL, -- 英文 Key(与数据库 CHECK 约束一致)
label_zh VARCHAR(60) NOT NULL, -- 中文标签(前端展示用)
sort_order SMALLINT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
remark TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_enum_labels_domain_value ON enum_labels(domain, value);
CREATE INDEX idx_enum_labels_domain ON enum_labels(domain, sort_order);
覆盖的枚举域(domain 清单)
| domain | 说明 | 对应表字段 |
|---|---|---|
client.status |
客源状态(8 态) | clients.status |
client.grade |
客源等级(5 档 + E) | clients.grade |
client.requirement_type |
需求类型(旧:client.purpose_type) |
client_requirements.requirement_type |
client.property_usage |
房源用途偏好(旧:client.usage) |
client_requirements.property_usage |
client.orientation |
朝向偏好 | client_requirements.orientation |
client.payment_method |
付款方式 | clients.payment_method |
property.status |
房源状态 | properties.status |
property.attribute |
房源属性(公/私/保护) | properties.attribute |
property.property_type |
房源类型(旧:property.usage) |
properties.property_type |
property.grade |
房源等级(5 档) | properties.grade |
property.listing_history.listing_type |
挂牌类型(旧:property.listing_type) |
listing_histories.listing_type |
property.decoration |
装修程度 | properties.decoration |
property.orientation |
朝向 | properties.orientation |
property.commission.status |
委托状态(旧:commission.type) |
commissions.status |
field_survey.status |
实勘状态 | field_surveys.status |
follow_log.log_type |
跟进日志类型 | follow_logs.log_type |
重要约定
enum_labels.value必须与对应表的CHECK约束完全一致,两者必须同步修改- 新增枚举值流程:① 修改 DDL
CHECK约束 → ② 插入enum_labels种子数据 → ③ 更新ENUMS.md is_active = FALSE仅停用前端展示,不得修改或删除已有value(历史数据引用不可破坏)- 前端下拉渲染统一从
enum_labels读取,禁止在前端代码中硬编码中文标签
与 lookup_items 的区别
| 对比维度 | enum_labels |
lookup_items |
|---|---|---|
| 用途 | 固定枚举的中文标签映射 | 运营可配置的动态选项(如跟进目的、来源渠道) |
| 修改权限 | 仅开发/DBA | 运营人员后台配置 |
| Schema 位置 | Public Schema(共享) | Tenant Schema(每租户独立) |
| 典型示例 | 客源状态、房源等级 | 跟进目的、客户来源渠道 |
五、关键索引汇总与查询优化策略
4.1 房源列表页核心查询分析
-- 典型查询:出售状态 + 公盘 + 特定区域 + 价格区间 + 户型筛选 + 按挂牌日期排序
-- 优化方案:复合索引覆盖最高频维度组合
-- 高频组合索引(status + attribute,覆盖 90% 的列表查询)
CREATE INDEX idx_properties_list_composite ON properties
(status, attribute, complex_id, sale_price DESC NULLS LAST)
WHERE deleted_at IS NULL;
-- 与我相关查询(经纪人个人仪表板)
CREATE INDEX idx_properties_my_properties ON properties
(seller_agent_id, status, listed_at DESC NULLS LAST)
WHERE deleted_at IS NULL;
4.2 全文搜索触发器(自动维护 search_vector)
-- 房源全文检索向量更新触发器
CREATE OR REPLACE FUNCTION update_property_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('simple', COALESCE(NEW.block_no, '') ||
' ' || COALESCE(NEW.unit_no, '') ||
' ' || COALESCE(NEW.room_no, '')), 'A') ||
setweight(to_tsvector('simple', COALESCE(NEW.remarks, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_property_search_vector
BEFORE INSERT OR UPDATE OF block_no, unit_no, room_no, remarks
ON properties
FOR EACH ROW EXECUTE FUNCTION update_property_search_vector();
-- 楼盘全文检索向量(含别名,提升模糊搜索精度)
CREATE OR REPLACE FUNCTION update_complex_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('simple', COALESCE(NEW.name, '')), 'A') ||
setweight(to_tsvector('simple', COALESCE(NEW.alias, '')), 'B') ||
setweight(to_tsvector('simple', COALESCE(NEW.address, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_complex_search_vector
BEFORE INSERT OR UPDATE OF name, alias, address
ON complexes
FOR EACH ROW EXECUTE FUNCTION update_complex_search_vector();
4.3 last_followed_at 自动维护触发器
-- 每次写入跟进日志时,自动更新 properties.last_followed_at
CREATE OR REPLACE FUNCTION update_property_last_followed()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.log_type = 'written' THEN
UPDATE properties
SET last_followed_at = NEW.created_at,
updated_at = NOW()
WHERE id = NEW.property_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_update_last_followed
AFTER INSERT ON follow_logs
FOR EACH ROW EXECUTE FUNCTION update_property_last_followed();
六、Redis 缓存策略
5.1 缓存 Key 规范
# 格式:{tenant_schema}:{module}:{entity}:{id}:{field}
# TTL 单位:秒
# 房源详情(高频读取)
{schema}:prop:detail:{property_id} TTL: 300 (5分钟)
# 房源联系人(含解密号码,敏感,TTL 短)
{schema}:prop:contacts:{property_id} TTL: 60 (1分钟)
# 楼盘基础信息(低变更频率)
{schema}:complex:base:{complex_id} TTL: 3600 (1小时)
# 楼盘名称自动补全候选列表(联想搜索)
{schema}:complex:autocomplete:{prefix} TTL: 600 (10分钟)
# 员工信息(用于日志快照)
{schema}:staff:base:{staff_id} TTL: 1800 (30分钟)
# 枚举值/lookup(几乎不变)
{schema}:lookup:{category_code} TTL: 86400 (24小时)
# 登录模块(详见 DATA_MODEL_LOGIN.md §四)
captcha_token:{uuid} TTL: 180 (3分钟)
captcha_pass:{uuid} TTL: 180 (3分钟)
login_fail:{tenant_id}:{username} TTL: 1800 (30分钟,连续失败计数)
recover_email:{email} TTL: 3600 (1小时,发送次数限流)
recover_reset:{account_id} TTL: 3600 (1小时,Token 生成次数限流)
tenant_verify_ip:{ip} TTL: 60 (1分钟,IP 限流)
# 权限模块(详见 DATA_MODEL_PERMISSION.md §六)
perm:v{VER}:{schema}:{staff_id} TTL: 3600 (员工完整权限快照)
perm:v{VER}:{schema}:role:{role_id}:staff_ids TTL: 3600 (角色→员工 ID 列表,批量失效用)
perm:inconsistent:{schema}:{staff_id} TTL: 300 (权限不一致标记)
perm:defs:{schema} TTL: 86400 (权限定义全量缓存)
perm:role_applied_count:{schema}:{role_id} TTL: 600 (角色应用人数)
# 标签列表
{schema}:tags:property TTL: 3600
# 维护完成度(Celery 计算后写入,详情页直接读 Redis)
{schema}:prop:completeness:{property_id} TTL: 600
# 房源列表计数(筛选后总条数,避免 COUNT(*) 全扫)
{schema}:prop:count:{filter_hash} TTL: 30 (短TTL,保证准确性)
5.2 缓存失效策略
# Django Signal 驱动的缓存失效(在 models.py 中注册)
# 房源更新 → 失效详情缓存 + 完成度缓存
# 跟进日志新增 → 失效 last_followed_at 缓存
# 联系人更新 → 失效联系人缓存(立即)
# 楼盘更新 → 失效楼盘缓存 + 相关房源缓存(批量)
# 枚举更新 → 失效对应 lookup 缓存
七、Django Model 层设计要点
6.1 抽象基类
# models/base.py
import uuid
from django.db import models
class UUIDPrimaryKeyModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Meta:
abstract = True
class TimeStampedModel(UUIDPrimaryKeyModel):
created_at = models.DateTimeField(auto_now_add=True, db_index=False)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class SoftDeleteModel(TimeStampedModel):
deleted_at = models.DateTimeField(null=True, blank=True, db_index=False)
class Meta:
abstract = True
def soft_delete(self, deleted_by=None):
from django.utils import timezone
self.deleted_at = timezone.now()
self.save(update_fields=['deleted_at', 'updated_at'])
class AuditedModel(SoftDeleteModel):
created_by = models.ForeignKey(
'staff.Staff', null=True, on_delete=models.SET_NULL,
related_name='+', db_column='created_by'
)
updated_by = models.ForeignKey(
'staff.Staff', null=True, on_delete=models.SET_NULL,
related_name='+', db_column='updated_by'
)
class Meta:
abstract = True
6.2 加密字段 Mixin
# utils/encryption.py
# 手机号加密:AES-256-GCM + SHA-256 哈希索引
class EncryptedPhoneField:
"""
存储时:phone → AES加密 → phone_enc (BYTEA)
phone → SHA256 → phone_hash (VARCHAR 64)
查询时:phone_hash 走索引,phone_enc 解密展示
打码展示:前3位明文 + ******* + 后3位
"""
pass
6.3 Manager 过滤软删除
class ActiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(deleted_at__isnull=True)
class PropertyManager(ActiveManager):
def public(self):
return self.get_queryset().filter(attribute='public')
def mine(self, staff_id):
return self.get_queryset().filter(seller_agent_id=staff_id)
八、数据量与性能预测
| 表名 | 预估行数 | 增长速度 | 分区策略 |
|---|---|---|---|
properties |
89,000+ | 中速 | 暂不分区,建议 500k 后按 created_at RANGE 分区 |
follow_logs |
200万+ | 高速(最高频写入) | ✅ PARTITION BY RANGE (created_at) 月度分区 |
property_photos |
500万+ | 高速 | ✅ PARTITION BY RANGE (created_at) 月度分区 |
permission_change_logs |
100万+ | 中高速 | ✅ PARTITION BY RANGE (operated_at) 月度分区 |
login_attempts |
500万+ | 高速(每次登录一条) | ✅ PARTITION BY RANGE (attempted_at) 月度分区 |
platform_audit_logs |
10万+ | 低中速 | ✅ PARTITION BY RANGE (created_at) 月度分区 |
price_changes |
50万 | 中速 | 无需分区 |
listing_histories |
20万 | 低速 | 无需分区 |
clients |
10万+ | 中速 | 暂不分区 |
viewings |
100万 | 中速 | 无需分区 |
8.1 分区维护策略(partition_maintenance_task)
所有月度分区表统一由 Celery Beat 定时任务 partition_maintenance_task 维护,每月 1 日凌晨 01:00(UTC+8)自动执行:
# apps/property/tasks.py(及 permission/login/shared 各 App 对应任务)
@app.task(name="partition_maintenance_task")
def partition_maintenance_task():
"""
为下一个月预建所有分区表的分区。
- 检查是否已存在目标分区,幂等执行
- 失败时发送 Sentry 告警
"""
tables = [
("follow_logs", "created_at"),
("property_photos", "created_at"),
("permission_change_logs", "operated_at"),
("login_attempts", "attempted_at"),
("public.platform_audit_logs", "created_at"),
]
next_month = date.today().replace(day=1) + relativedelta(months=1)
month_start = next_month
month_end = next_month + relativedelta(months=1)
for table, _key in tables:
suffix = month_start.strftime("%Y_%m")
part_name = f"{table.replace('.', '_')}_{suffix}"
sql = f"""
CREATE TABLE IF NOT EXISTS {part_name}
PARTITION OF {table}
FOR VALUES FROM ('{month_start}') TO ('{month_end}');
"""
with connection.cursor() as cursor:
cursor.execute(sql)
Celery Beat 配置(celery.py):
app.conf.beat_schedule["partition_maintenance_task"] = {
"task": "partition_maintenance_task",
"schedule": crontab(day_of_month=1, hour=1, minute=0), # 每月1日 01:00 UTC+8
}
⚠️ 注意:每张分区表均保留一个
_default默认分区作为兜底,防止任务失败时写入报错。_default分区数据应在运维 SOP 中周期性检查(有数据则说明提前建分区失败)。
九、必须在开发启动前明确的数据架构决策
| 决策项 | 推荐方案 | 风险 |
|---|---|---|
| 小区数据来源 | 预导入基础数据(安居客/链家 API)+ 支持手动新增兜底 | 高:影响录入体验 |
| 私盘可见范围 | 录入人所在门店可见(综合业务需求) | 需与权限模块约定 |
| 号码查看权限 | 角色级控制:经纪人限查自己相关房源,店长无限制 | 需合规确认 |
| 重复房源主键 | 主键:手机号 hash;辅助:(小区+楼栋+单元+房号)组合 | 需双重校验 |
| 跟进目的枚举 | 存 lookup_items 表,运营可维护 | 初始化数据需提前收集 |
| 手机号加密算法 | AES-256-GCM,密钥存 Django settings(生产用 Vault) | 密钥管理需单独规划 |
本文档为 Fonrey 系统 DATA MODEL v1.0,随 PRD 迭代同步更新。 下一步建议:API 接口规范(URL 设计 + Request/Response Schema)