> **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_SETTING) > **定位**:本文件是 `apps/setting/` 模块的数据模型权威来源。 > **版本**:v1.0 | **日期**:2026-04-27 > **关联 PRD**:`PRD/系统配置/系统配置模块PRD.md` > **关联文档**:`DATA_MODEL/ENUMS.md`、`DATA_MODEL/DATA_MODEL.md` --- ## 一、模块定位与架构边界 ### 1.1 系统配置模块职责 系统配置模块(`apps/setting/`)负责管理三类性质不同的配置数据: | 类型 | 说明 | 表 | Schema | | -------------- | ---------------- | --------------------------------------------- | -------------- | | **A. 固定系统枚举** | 平台级固定值域,所有租户共享 | `enum_labels` | Public(shared) | | **B. 可配置枚举** | 各租户选项不同,管理员可增删排序 | `lookup_groups` + `lookup_items` | Tenant | | **C. 行为规则与开关** | 标量配置开关 + 字段必填规则 | `tenant_settings` + `field_requirement_rules` | Tenant | > **重要区分**: > - 类型 A (`enum_labels`) 已在 `DATA_MODEL/ENUMS.md` 完整定义,**本文件不重复** > - 类型 B/C 均存于 **租户 Schema**,由租户管理员通过界面维护 > - `apps/setting/` 是 `tenant_apps`(**非** `shared_apps`) ### 1.2 依赖关系 ``` apps/setting/ ├── 依赖 → core.cache(Redis,统一租户前缀) ├── 依赖 → org.Staff(created_by / updated_by FK) └── 被依赖 ← apps/property(读取字段规则、枚举选项) ← apps/client(读取查重范围、枚举选项) ``` --- ## 二、可配置枚举表(类型 B) ### 2.1 `lookup_groups`(枚举分组) 每个分组代表一类可配置枚举(如「客源来源」「跟进目的」),由研发预置,租户管理员**不可新增或删除分组**,仅可管理分组内的选项。 ```sql -- ============================================================ -- 可配置枚举分组(Tenant Schema) -- 研发预置,租户不可修改分组本身 -- ============================================================ CREATE TABLE lookup_groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), module VARCHAR(50) NOT NULL, -- 'client' | 'property' key VARCHAR(100) NOT NULL, -- 'source' | 'follow_purpose' label_zh VARCHAR(50) NOT NULL, -- 界面显示名称,如「客源来源」 description TEXT, -- 说明文案(前端 tooltip 使用) sort_order SMALLINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (module, key) ); ``` **MVP 预置分组(种子数据)**: | module | key | label_zh | description | |--------|-----|----------|-------------| | `client` | `source` | 客源来源 | 客源从何处获取,用于来源渠道分析 | | `client` | `follow_purpose` | 跟进目的 | 客源跟进时选择的目的分类 | | `property` | `source` | 房源来源 | 房源从何处获取 | --- ### 2.2 `lookup_items`(枚举选项) ```sql -- ============================================================ -- 可配置枚举选项(Tenant Schema) -- 租户管理员可增删排序;is_system=True 的预制项不可物理删除 -- ============================================================ CREATE TABLE lookup_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), group_id UUID NOT NULL REFERENCES lookup_groups(id) ON DELETE CASCADE, value VARCHAR(100) NOT NULL, -- 存储值,英文 snake_case(如 'door_to_door') label_zh VARCHAR(50) NOT NULL, -- 显示文本(如「上门」) is_system BOOLEAN NOT NULL DEFAULT FALSE, -- True=系统预制,不可删除,仅可停用 is_active BOOLEAN NOT NULL DEFAULT TRUE, sort_order SMALLINT NOT NULL DEFAULT 0, created_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 系统预制时为 NULL created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (group_id, value) ); CREATE INDEX idx_lookup_items_group_active ON lookup_items(group_id, is_active, sort_order); ``` **关键约束**: - `is_system = TRUE` 的记录不允许物理删除(Service 层强制拦截) - `is_active = FALSE` 后:前端录入下拉不展示;历史已选该值的记录保留原值,展示时追加「(已停用)」后缀 - `value` 一旦写入不允许修改(历史数据依赖);如需改名,停用旧项、新增新项 --- ### 2.3 MVP 预置种子数据(`is_system = TRUE`) 以下选项在租户初始化时自动写入: #### 客源来源(`client.source`) | value | label_zh | sort_order | |-------|----------|------------| | `store_reception` | 门店接待 | 1 | | `old_client_referral` | 老客户转介绍 | 2 | | `stationed_dispatch` | 驻守派单 | 3 | | `walk_in` | 上门 | 4 | | `online_58` | 网络-58同城 | 5 | | `online_anjuke` | 网络-安居客 | 6 | | `wechat` | 微信 | 7 | | `friend_referral` | 朋友介绍 | 8 | #### 跟进目的(`client.follow_purpose`) | value | label_zh | sort_order | |-------|----------|------------| | `callback` | 回拨 | 1 | | `push_property` | 推房 | 2 | | `showing` | 带看 | 3 | | `maintain` | 维护 | 4 | | `other` | 其他 | 5 | #### 房源来源(`property.source`) | value | label_zh | sort_order | |-------|----------|------------| | `proactive_development` | 主动开发 | 1 | | `owner_walk_in` | 业主上门 | 2 | | `old_client_referral` | 老客户转介绍 | 3 | | `online_inquiry` | 网络来电 | 4 | --- ## 三、行为规则与开关(类型 C) ### 3.1 `tenant_settings`(标量配置键值表) 存储开关(bool)、阈值(int)、单选枚举(string)等标量类型配置项。 ```sql -- ============================================================ -- 租户标量配置(键值对)(Tenant Schema) -- ============================================================ CREATE TABLE tenant_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), category VARCHAR(50) NOT NULL, -- 配置分类:'client' | 'property' | 'showroom' key VARCHAR(100) NOT NULL, -- 配置 key,如 'duplicate_check_scope' value JSONB NOT NULL, -- 存储任意类型(bool/int/str),如 {"v": "self"} value_type VARCHAR(20) NOT NULL -- 'bool' | 'int' | 'string' | 'enum'(用于前端渲染控件) CHECK (value_type IN ('bool', 'int', 'string', 'enum')), updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (category, key) ); CREATE INDEX idx_tenant_settings_category ON tenant_settings(category); ``` **存储格式约定**: - `bool`:`{"v": true}` 或 `{"v": false}` - `int`:`{"v": 30}` - `string`:`{"v": "some_value"}` - `enum`:`{"v": "self", "choices": ["self", "dept", "company"]}` — `choices` 由代码硬编码,不存 DB **MVP 阶段预置 key**: | category | key | value_type | 默认值 | 说明 | |----------|-----|-----------|--------|------| | `client` | `duplicate_check_scope` | `enum` | `{"v": "self"}` | 新增私客查重范围:`self`(本人)/ `dept`(本部门)/ `company`(全公司) | > 未来 P1 阶段可按需追加 key,无需修改表结构。 --- ### 3.2 `field_requirement_rules`(字段必填规则表) 按「模块 × 房源用途 × 交易状态 × 字段」四元组确定一条规则,控制录入界面的字段显示状态。 ```sql -- ============================================================ -- 字段必填/隐藏规则(Tenant Schema) -- MVP 仅支持 module='property' -- ============================================================ CREATE TABLE field_requirement_rules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), module VARCHAR(20) NOT NULL, -- 'property' | 'client'(MVP 只用 'property') entity_type VARCHAR(50) NOT NULL, -- 与 property.property_type CHECK 约束值对齐 -- 'residential'|'villa'|'commercial_residential'|'shop'|'office'|'other' trade_status VARCHAR(20) NOT NULL, -- 交易大类:'sale'|'rent'|'sale_rent'|'*'(全部) CHECK (trade_status IN ('sale', 'rent', 'sale_rent', '*')), field_key VARCHAR(50) NOT NULL, -- 字段 key,如 'orientation'|'decoration'|'floor' requirement VARCHAR(10) NOT NULL -- 规则值 CHECK (requirement IN ('required', 'optional', 'hidden')), updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (module, entity_type, trade_status, field_key) ); CREATE INDEX idx_field_req_lookup ON field_requirement_rules(module, entity_type, trade_status); ``` **与 `property.property_type` 对齐说明**: `entity_type` 的值域与 `property.property_type` 的 CHECK 约束完全一致: | entity_type | property_type label_zh | |-------------|----------------------| | `residential` | 住宅 | | `villa` | 别墅 | | `commercial_residential` | 商住 | | `shop` | 商铺 | | `office` | 写字楼 | | `other` | 其他 | **`trade_status` 与 `property.status` 的映射关系**: | trade_status | 对应 property.status 值 | |--------------|------------------------| | `sale` | `for_sale` | | `rent` | `for_rent` | | `sale_rent` | `for_sale_rent` | | `*` | 所有状态通用规则(fallback) | > **重要**:`trade_status` 是录入场景的交易意图分类,不是 `property.status` 的完整枚举。规则匹配逻辑:先查精确匹配(`entity_type + trade_status`),不存在则查 `*` 通配规则。 **MVP 初始规则(研发预置,管理员可覆盖)**: | module | entity_type | trade_status | field_key | requirement | |--------|-------------|--------------|-----------|-------------| | `property` | `residential` | `sale` | `orientation` | `optional` | | `property` | `residential` | `sale` | `decoration` | `optional` | | `property` | `residential` | `sale` | `floor` | `optional` | | `property` | `residential` | `sale` | `building_area` | `required` | | `property` | `residential` | `sale` | `inner_area` | `optional` | | `property` | `residential` | `sale` | `room_layout` | `required` | | `property` | `residential` | `rent` | `decoration` | `optional` | | `property` | `residential` | `rent` | `floor` | `optional` | | `property` | `residential` | `rent` | `building_area` | `required` | | `property` | `residential` | `rent` | `room_layout` | `required` | **MVP 可配置字段范围(对应 PRD AC-4)**: | field_key | 说明 | 字段类型 | |-----------|------|---------| | `orientation` | 朝向(`property.orientation` 枚举) | 枚举 | | `decoration` | 装修情况(`property.decoration` 枚举) | 枚举 | | `floor` | 所在楼层/总楼层 | 数值 | | `building_area` | 建筑面积(㎡) | 数值 | | `inner_area` | 套内面积(㎡) | 数值 | | `room_layout` | 房型(室/厅/卫) | 数值组 | | `ownership_years` | 产权年限(年) | 数值 | | `parking_count` | 车位数 | 数值 | --- ## 四、服务层设计 所有业务模块**禁止直接查询配置表**,必须通过统一服务层读取: ```python # apps/setting/services/tenant_settings_service.py class TenantSettingsService: """ 系统配置统一读取服务。 所有配置均经 Redis 缓存,TTL 5min,写入时主动 invalidate。 Redis Key 规范:{tenant_schema}:setting:{type}:{key} """ def get(self, category: str, key: str, default=None): """ 读取标量配置(tenant_settings 表) Cache Key: {tenant_schema}:setting:kv:{category}.{key} 返回 JSONB value 字段中 'v' 的值(已解包) """ def set(self, category: str, key: str, value, updated_by_id) -> None: """ 写入标量配置 + 主动 invalidate 缓存 """ def get_lookup_items(self, module: str, key: str) -> list[dict]: """ 获取可配置枚举选项(lookup_items 表) 仅返回 is_active=True 的项,按 sort_order 排序 Cache Key: {tenant_schema}:setting:lookup:{module}.{key} 返回格式:[{"value": "walk_in", "label_zh": "上门", "is_system": True}, ...] """ def get_field_requirements( self, module: str, entity_type: str, trade_status: str ) -> dict[str, str]: """ 获取字段必填规则 匹配顺序:精确匹配(entity_type + trade_status) > 通配规则(trade_status='*') Cache Key: {tenant_schema}:setting:field_req:{module}.{entity_type}.{trade_status} 返回格式:{"orientation": "optional", "decoration": "required", ...} """ ``` --- ## 五、Redis 缓存键规范 | 用途 | Cache Key | TTL | 失效触发 | |------|-----------|-----|---------| | 标量配置 | `{schema}:setting:kv:{category}.{key}` | 300s | `TenantSettingsService.set()` | | 可配置枚举 | `{schema}:setting:lookup:{module}.{key}` | 300s | 管理员保存 lookup_items | | 字段规则 | `{schema}:setting:field_req:{module}.{entity_type}.{trade_status}` | 300s | 管理员保存 field_requirement_rules | | 客源规则(整体) | `{schema}:setting:client_rules` | 300s | 任意客源规则变更 | > TTL 300s(5 分钟)对应 PRD 成功指标「配置变更生效时延 ≤ 5 分钟」。 --- ## 六、目录结构 ``` apps/setting/ ├── models/ │ ├── lookup.py # LookupGroup, LookupItem │ └── setting.py # TenantSetting, FieldRequirementRule ├── services/ │ └── tenant_settings_service.py # 统一配置读取服务(禁止直接查表) ├── views/ │ ├── lookup_views.py # 参数配置页面(US-SETTING-001-A) │ ├── field_rule_views.py # 房源字段规则(US-SETTING-001-B) │ └── client_rule_views.py # 客源规则(US-SETTING-001-C) ├── templates/setting/ ├── fixtures/ │ ├── lookup_groups.json # 分组种子数据(3 组) │ ├── lookup_items.json # 选项种子数据(is_system=True) │ ├── tenant_settings.json # 默认配置种子数据 │ └── field_requirement_rules.json # 默认字段规则 ├── migrations/ │ ├── 0001_lookup_groups.py │ ├── 0002_lookup_items.py │ ├── 0003_tenant_settings.py │ └── 0004_field_requirement_rules.py └── urls.py ``` --- ## 七、迁移执行顺序 ``` 0001_lookup_groups # 先建分组表(无外键依赖) 0002_lookup_items # 再建选项表(依赖 lookup_groups + staff) 0003_tenant_settings # 独立,无外键依赖 0004_field_requirement_rules # 独立,仅依赖 staff ``` 迁移执行后,通过 `call_command('loaddata', 'lookup_groups')` 等方式加载 fixtures 种子数据。 --- ## 八、关键约束与禁止项 | 约束 | 规则 | |------|------| | 不可删除系统预制项 | `lookup_items.is_system = True` 的记录,Service 层硬拦截物理删除请求 | | 不可修改已有 value | `lookup_items.value` 写入后只读;修改请停用旧项 + 新增新项 | | 不可直接查询配置表 | 业务模块(property/client)**必须**通过 `TenantSettingsService` 读取,禁止 ORM 直查 | | entity_type 值域 | 必须与 `property.property_type` CHECK 约束完全一致(见第三章) | | Redis Key 前缀 | 必须携带租户 schema 前缀,格式:`{tenant_schema}:setting:{type}:{key}` | --- ## 九、设计决策(ADR) | 决策 | 选择 | 理由 | |------|------|------| | 枚举两级架构 | `enum_labels`(Public/固定)+ `lookup_items`(Tenant/可配置)分离 | 保障系统一致性的同时给租户灵活度 | | `lookup_groups` 由研发预置 | 是 | 防止租户随意创建不规范分组,控制可配置范围边界 | | `tenant_settings` 使用 JSONB | 是 | 支持 bool/int/string 等多类型,无需为每类型单独建列 | | `field_requirement_rules` 不新增字段 | 是 | 规则层只控制「必填/选填/隐藏」,字段存在性由 DATA_MODEL_PROPERTY 决定 | | 服务层统一读取 | `TenantSettingsService` | 统一缓存管理,业务层与配置存储解耦 | | trade_status 不复用 property.status | 独立 `sale/rent/sale_rent/*` | 录入场景的交易意图与房源全生命周期状态不同,避免耦合 |