Files
nexus/Project/fonrey/DATA_MODEL/DATA_MODEL_SETTING.md
2026-04-30 20:33:51 +08:00

398 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> **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`
---
## 变更历史
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
## 一、模块定位与架构边界
### 1.1 系统配置模块职责
系统配置模块(`apps/setting/`)负责管理三类性质不同的配置数据:
| 类型 | 说明 | 表 | Schema |
| -------------- | ---------------- | --------------------------------------------- | -------------- |
| **A. 固定系统枚举** | 平台级固定值域,所有租户共享 | `enum_labels` | Publicshared |
| **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.cacheRedis统一租户前缀
├── 依赖 → org.Staffcreated_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, -- 所属枚举分组(关联 lookup_groups
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, -- 是否启用FALSE 后前端下拉不展示,历史数据保留并追加「(已停用)」后缀
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, -- 最后修改人(关联 staff 表)
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, -- 最后修改人(关联 staff 表)
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 300s5 分钟)对应 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/*` | 录入场景的交易意图与房源全生命周期状态不同,避免耦合 |