398 lines
18 KiB
Markdown
398 lines
18 KiB
Markdown
> **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` | 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, -- 所属枚举分组(关联 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 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/*` | 录入场景的交易意图与房源全生命周期状态不同,避免耦合 |
|