Sync: expand data model and gitops notes
This commit is contained in:
@@ -70,7 +70,59 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 二、公共 Schema(Shared / Public)
|
## 二、领域概览(Domain Overview)
|
||||||
|
|
||||||
|
本节用业务语言描述系统的核心领域对象及其关系,作为各子模块数据模型的导读。
|
||||||
|
|
||||||
|
### 核心领域对象
|
||||||
|
|
||||||
|
| 领域对象 | 表/子文档 | 业务说明 |
|
||||||
|
|----------|-----------|----------|
|
||||||
|
| **Tenant(租户)** | `public.tenants` | 每家房产经纪公司对应一个租户,数据完全隔离(Schema-per-Tenant) |
|
||||||
|
| **OrgUnit(组织架构)** | `org_units` → [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 树形组织架构(总部/区域/城市/大区/分公司/门店/团队/虚拟团队),物化路径存储,支持权限继承 |
|
||||||
|
| **Staff(员工)** | `staff` → [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 经纪人/店长/经理,绑定组织节点,手机号加密存储,与账号(登录)分离 |
|
||||||
|
| **District(城区)** | `districts` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 行政区划,如「静安区」,是区域体系的顶层节点 |
|
||||||
|
| **BusinessArea(商圈)** | `business_areas` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 商圈/板块,从属于城区,一个楼盘可归属多个商圈 |
|
||||||
|
| **School(学校)** | `schools` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 对口学校数据库,是买家购房决策的核心参考,与楼盘多对多关联 |
|
||||||
|
| **Complex(楼盘/小区)** | `complexes` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 房源录入的基础底座,维护楼盘标准名称/坐标/锁定状态/别名等 |
|
||||||
|
| **Building(楼栋/单元)** | `buildings` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼盘下的物理楼栋,区分标准结构与非标结构 |
|
||||||
|
| **RoomUnit(房号)** | `room_units` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼层+房间号,房源定位的最细粒度 |
|
||||||
|
| **Property(房源)** | `properties` → §3.3 | 系统核心表,每套二手房源的完整档案,支持出售/出租/出售兼出租三态 |
|
||||||
|
| **Client(客源)** | `clients` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 买家/租客档案,分私客/公客/成交客,含活跃度评分与自动公客转换机制 |
|
||||||
|
| **Viewing(带看)** | `client_viewings` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 经纪人带客户看房的完整记录 |
|
||||||
|
| **Match(配对)** | `client_property_matches` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 系统/人工推荐的客源↔房源配对 |
|
||||||
|
|
||||||
|
### 领域关系快速导航
|
||||||
|
|
||||||
|
```
|
||||||
|
District (城区)
|
||||||
|
└─ BusinessArea (商圈)
|
||||||
|
└─ Complex (楼盘) ─── School (对口学校)
|
||||||
|
├─ Building (楼栋)
|
||||||
|
│ └─ RoomUnit (房号)
|
||||||
|
└─ Property (房源)
|
||||||
|
├─ PropertyContact (联系人/委托方)
|
||||||
|
├─ FollowLog (跟进日志)
|
||||||
|
├─ Viewing (带看记录) ──── Client (客源)
|
||||||
|
└─ Match (配对记录) ──────┘
|
||||||
|
|
||||||
|
OrgUnit (组织架构)
|
||||||
|
└─ Staff (员工/经纪人) ─── Property / Client / Viewing / Match
|
||||||
|
```
|
||||||
|
|
||||||
|
### 子文档索引
|
||||||
|
|
||||||
|
| 子文档 | 覆盖模块 | 状态 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 组织人事(org_units, staff, 异动/奖惩/教育/家庭等) | ✅ 完成 |
|
||||||
|
| [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼盘/区域(districts, business_areas, complexes, buildings, room_units, schools 等) | ✅ 完成 |
|
||||||
|
| [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 客源管理(clients, requirements, follow_logs, viewings, matches 等) | ✅ 完成 |
|
||||||
|
| 本文档 §3.3–§3.16 | 房源核心(properties 及配套 12 张表)、系统设置 | ✅ 完成 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、公共 Schema(Shared / Public)
|
||||||
|
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
@@ -107,7 +159,7 @@ CREATE INDEX idx_domains_primary ON public.domains(tenant_id) WHERE is_primary =
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 三、租户 Schema(Tenant Schema)
|
## 四、租户 Schema(Tenant Schema)
|
||||||
|
|
||||||
以下所有表均在每个租户的独立 Schema 内创建。
|
以下所有表均在每个租户的独立 Schema 内创建。
|
||||||
|
|
||||||
@@ -115,190 +167,56 @@ CREATE INDEX idx_domains_primary ON public.domains(tenant_id) WHERE is_primary =
|
|||||||
|
|
||||||
### 3.1 组织人事模块(Organization & HR)
|
### 3.1 组织人事模块(Organization & HR)
|
||||||
|
|
||||||
```sql
|
> **详细模型** → 见 [`DATA_MODEL_ORG.md`](./DATA_MODEL_ORG.md)
|
||||||
-- ============================================================
|
> 该文件为权威定义,包含完整字段、枚举、查询模式和禁止操作。
|
||||||
-- 组织架构:公司 → 区域 → 门店 → 组
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- 组织节点表(树形结构,支持无限层级)
|
**核心表概览**(开发时以 DATA_MODEL_ORG.md 为准):
|
||||||
CREATE TABLE org_units (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
type VARCHAR(20) NOT NULL
|
|
||||||
CHECK (type IN ('company','region','store','group')),
|
|
||||||
parent_id UUID REFERENCES org_units(id) ON DELETE RESTRICT,
|
|
||||||
path TEXT NOT NULL, -- 物化路径:/root_id/parent_id/self_id/
|
|
||||||
depth SMALLINT NOT NULL DEFAULT 0,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_org_units_parent ON org_units(parent_id) WHERE deleted_at IS NULL;
|
| 表名 | 说明 |
|
||||||
CREATE INDEX idx_org_units_path ON org_units USING gist(path gist_trgm_ops);
|
|------|------|
|
||||||
-- 注:gist_trgm_ops 需要 pg_trgm 扩展,用于路径前缀查询
|
| `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安居客/中国网络经纪人等) |
|
||||||
|
|
||||||
-- 员工表
|
**关键约束提示**:
|
||||||
CREATE TABLE staff (
|
- `staff.phone_enc` AES-256-GCM 加密,`staff.phone_hash` SHA-256 用于唯一索引
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
- `staff_transfer_logs` **无 deleted_at**,不可删除
|
||||||
org_unit_id UUID NOT NULL REFERENCES org_units(id) ON DELETE RESTRICT,
|
- `org_units` 路径查询:`WHERE path LIKE '/root/{target_id}/%'`
|
||||||
name VARCHAR(50) NOT NULL,
|
- 员工离职:`status = 'resigned'` + `deleted_at` 软删除,记录永久保留
|
||||||
phone_hash VARCHAR(64), -- SHA-256 哈希,用于唯一性校验
|
|
||||||
phone_enc BYTEA, -- AES-256-GCM 加密后的手机号
|
|
||||||
email VARCHAR(255),
|
|
||||||
role VARCHAR(30) NOT NULL
|
|
||||||
CHECK (role IN ('agent','store_manager','admin','operator','system')),
|
|
||||||
job_title VARCHAR(100), -- 职务描述
|
|
||||||
avatar_key TEXT, -- R2/S3 存储路径
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
joined_at DATE,
|
|
||||||
left_at DATE,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ,
|
|
||||||
|
|
||||||
-- 关联 Django auth user(用于登录认证)
|
|
||||||
user_id INTEGER UNIQUE -- FK to django auth_user
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_staff_org ON staff(org_unit_id) WHERE deleted_at IS NULL;
|
|
||||||
CREATE INDEX idx_staff_role ON staff(role) WHERE deleted_at IS NULL;
|
|
||||||
CREATE UNIQUE INDEX idx_staff_phone_hash ON staff(phone_hash) WHERE deleted_at IS NULL;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.2 区域与楼盘模块(Region & Complex Management)
|
### 3.2 区域与楼盘模块(Region & Complex Management)
|
||||||
|
|
||||||
```sql
|
> **详细模型** → 见 [`DATA_MODEL_COMPLEX.md`](./DATA_MODEL_COMPLEX.md)
|
||||||
-- ============================================================
|
> 本节仅作概览,开发时以 DATA_MODEL_COMPLEX.md 为权威定义。
|
||||||
-- 行政区 → 商圈 → 楼盘/小区 → 楼栋
|
|
||||||
-- 注:楼盘数据是房源录入的基础底座,数据质量直接影响房源录入效率
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- 城市/行政区
|
**核心表概览**(开发时以 DATA_MODEL_COMPLEX.md 为准):
|
||||||
CREATE TABLE districts (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name VARCHAR(50) NOT NULL,
|
|
||||||
city VARCHAR(50) NOT NULL DEFAULT '',
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 商圈/板块
|
| 表名 | 说明 | 关键字段 |
|
||||||
CREATE TABLE business_areas (
|
|------|------|----------|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
| `districts` | 城区/行政区 | `city`, `name`, `short_name`, `sort_order` |
|
||||||
district_id UUID NOT NULL REFERENCES districts(id) ON DELETE RESTRICT,
|
| `business_areas` | 商圈/板块(从属于城区) | `district_id`, `name`, `latitude`, `longitude` |
|
||||||
name VARCHAR(100) NOT NULL,
|
| `metro_lines` | 地铁线路 | `city`, `name`, `color` |
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
| `metro_stations` | 地铁站点 | `metro_line_id`, `name`, `latitude`, `longitude` |
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
| `schools` | 学校(对口学区) | `district_id`, `name`, `type`, `nature`, `level` |
|
||||||
);
|
| `complexes` | 楼盘/小区(房源底座) | `name`, `district_id`, `address`, `latitude/longitude`, `lock_*`, `search_vector` |
|
||||||
|
| `complex_aliases` | 楼盘别名(含系统别名/用户自定义别名) | `complex_id`, `alias`, `is_system` |
|
||||||
CREATE INDEX idx_business_areas_district ON business_areas(district_id);
|
| `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` |
|
||||||
CREATE TABLE metro_lines (
|
| `buildings` | 楼栋/单元 | `complex_id`, `name`, `is_standard`, `total_floors` |
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
| `room_units` | 房号/结构单元(楼层+房间号) | `building_id`, `floor`, `room_no`, `is_standard` |
|
||||||
name VARCHAR(50) NOT NULL,
|
| `complex_photos` | 楼盘照片(楼盘图/户型图/VR) | `complex_id`, `category`, `file_key`, `is_cover` |
|
||||||
color VARCHAR(7) -- 线路颜色 HEX
|
| `complex_attachments` | 楼盘附件 | `complex_id`, `file_key`, `file_name` |
|
||||||
);
|
| `complex_price_trends` | 楼盘价格走势(月度) | `complex_id`, `record_month`, `avg_unit_price` |
|
||||||
|
|
||||||
-- 地铁站
|
|
||||||
CREATE TABLE metro_stations (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
metro_line_id UUID NOT NULL REFERENCES metro_lines(id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(50) NOT NULL,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 学校
|
|
||||||
CREATE TABLE schools (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
district_id UUID REFERENCES districts(id) ON DELETE SET NULL,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
type VARCHAR(20) -- 小学/初中/高中/九年一贯制 等
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_schools_district ON schools(district_id);
|
|
||||||
|
|
||||||
-- 楼盘/小区(核心基础表)
|
|
||||||
CREATE TABLE complexes (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name VARCHAR(200) NOT NULL,
|
|
||||||
alias VARCHAR(200), -- 别名/曾用名
|
|
||||||
district_id UUID REFERENCES districts(id) ON DELETE SET NULL,
|
|
||||||
business_area_id UUID REFERENCES business_areas(id) ON DELETE SET NULL,
|
|
||||||
address VARCHAR(500),
|
|
||||||
latitude NUMERIC(10,7),
|
|
||||||
longitude NUMERIC(10,7),
|
|
||||||
|
|
||||||
-- 楼盘物理属性
|
|
||||||
developer VARCHAR(200), -- 开发商
|
|
||||||
property_company VARCHAR(200), -- 物业公司
|
|
||||||
property_fee NUMERIC(8,2), -- 物业费 元/㎡/月
|
|
||||||
green_rate NUMERIC(5,2), -- 绿化率 %
|
|
||||||
plot_ratio NUMERIC(5,2), -- 容积率
|
|
||||||
built_year SMALLINT, -- 竣工年份
|
|
||||||
ownership_years VARCHAR(20), -- 产权年限枚举
|
|
||||||
|
|
||||||
-- 配套信息
|
|
||||||
has_elevator BOOLEAN,
|
|
||||||
parking_info TEXT, -- 车位情况描述
|
|
||||||
|
|
||||||
-- 全文检索向量(定期更新)
|
|
||||||
search_vector TSVECTOR,
|
|
||||||
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ,
|
|
||||||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_complexes_district ON complexes(district_id) WHERE deleted_at IS NULL;
|
|
||||||
CREATE INDEX idx_complexes_name_trgm ON complexes USING gin(name gin_trgm_ops);
|
|
||||||
CREATE INDEX idx_complexes_search ON complexes USING gin(search_vector);
|
|
||||||
CREATE INDEX idx_complexes_geo ON complexes(latitude, longitude) WHERE deleted_at IS NULL;
|
|
||||||
|
|
||||||
-- 楼盘与商圈多对多(一个楼盘可跨多个商圈)
|
|
||||||
CREATE TABLE complex_business_areas (
|
|
||||||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
|
||||||
business_area_id UUID NOT NULL REFERENCES business_areas(id) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (complex_id, business_area_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 楼盘与学校关联
|
|
||||||
CREATE TABLE complex_schools (
|
|
||||||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
|
||||||
school_id UUID NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
|
|
||||||
school_zone VARCHAR(50), -- 学区情况:对口/参考等
|
|
||||||
PRIMARY KEY (complex_id, school_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 楼盘与地铁站关联
|
|
||||||
CREATE TABLE complex_metro_stations (
|
|
||||||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
|
||||||
station_id UUID NOT NULL REFERENCES metro_stations(id) ON DELETE CASCADE,
|
|
||||||
distance_meters INTEGER, -- 步行距离(米)
|
|
||||||
PRIMARY KEY (complex_id, station_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 楼栋
|
|
||||||
CREATE TABLE buildings (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(50) NOT NULL, -- 楼栋名,如"1号楼"
|
|
||||||
total_floors SMALLINT NOT NULL,
|
|
||||||
has_elevator BOOLEAN,
|
|
||||||
building_type VARCHAR(30), -- 楼型:板楼/塔楼/板塔结合
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_buildings_complex ON buildings(complex_id);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1066,123 +984,30 @@ CREATE TABLE property_completeness (
|
|||||||
|
|
||||||
### 3.17 客源管理(Client Management)
|
### 3.17 客源管理(Client Management)
|
||||||
|
|
||||||
```sql
|
> **详细模型** → 见 [`DATA_MODEL_CLIENT.md`](./DATA_MODEL_CLIENT.md)
|
||||||
-- ============================================================
|
> 该文件为权威定义,包含完整字段、枚举、状态机、查询模式和禁止操作。
|
||||||
-- 客源:私客为核心,公客/成交客为后续版本
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
CREATE TABLE clients (
|
**核心表概览**(开发时以 DATA_MODEL_CLIENT.md 为准):
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
|
|
||||||
client_type VARCHAR(20) NOT NULL DEFAULT 'private'
|
| 表名 | 说明 |
|
||||||
CHECK (client_type IN ('private','public','transacted')),
|
|------|------|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
| `clients` | 客源主表(私客/公客/成交客),含加密手机号哈希、活跃度、归属人 |
|
||||||
CHECK (status IN ('active','converted_public',
|
| `client_contacts` | 联系人(1:N),手机号加密+哈希,支持多联系人 |
|
||||||
'transacted','invalid')),
|
| `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` | 意向学校(拆表,支持精确查询) |
|
||||||
|
|
||||||
name VARCHAR(50) NOT NULL,
|
**关键约束提示**:
|
||||||
gender VARCHAR(10)
|
- `client_contacts.phone_hash` 是重复客源检测的唯一依据,录入前必须查重
|
||||||
CHECK (gender IN ('male','female','unknown')),
|
- `client_status_logs` **无 deleted_at**,不可删除
|
||||||
|
- 私客超时(配置天数内无跟进)→ Celery 自动转公(`transfer_to_public_type = 'auto'`)
|
||||||
-- 手机号加密存储
|
- 活跃度 `activity_level` 由 Celery 每日凌晨批量计算,不实时更新
|
||||||
phone_enc BYTEA NOT NULL,
|
|
||||||
phone_hash VARCHAR(64) NOT NULL,
|
|
||||||
phone2_enc BYTEA,
|
|
||||||
phone2_hash VARCHAR(64),
|
|
||||||
|
|
||||||
-- 购房需求
|
|
||||||
purpose VARCHAR(10) NOT NULL
|
|
||||||
CHECK (purpose IN ('buy','rent')),
|
|
||||||
budget_min NUMERIC(12,2),
|
|
||||||
budget_max NUMERIC(12,2),
|
|
||||||
area_min NUMERIC(8,2),
|
|
||||||
area_max NUMERIC(8,2),
|
|
||||||
bedroom_needs SMALLINT[], -- 可接受的卧室数量数组
|
|
||||||
|
|
||||||
-- 意向区域(存 district/business_area ID 数组)
|
|
||||||
district_ids UUID[],
|
|
||||||
business_area_ids UUID[],
|
|
||||||
|
|
||||||
-- 活跃度分层(由系统计算)
|
|
||||||
activity_level VARCHAR(10)
|
|
||||||
CHECK (activity_level IN ('hot','warm','cold','frozen')),
|
|
||||||
last_active_at TIMESTAMPTZ,
|
|
||||||
|
|
||||||
-- 负责经纪人
|
|
||||||
agent_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
|
||||||
org_unit_id UUID REFERENCES org_units(id) ON DELETE SET NULL,
|
|
||||||
|
|
||||||
source VARCHAR(50),
|
|
||||||
remarks TEXT,
|
|
||||||
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ,
|
|
||||||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_clients_agent ON clients(agent_id) WHERE deleted_at IS NULL;
|
|
||||||
CREATE INDEX idx_clients_phone_hash ON clients(phone_hash) WHERE deleted_at IS NULL;
|
|
||||||
CREATE INDEX idx_clients_status ON clients(status, client_type) WHERE deleted_at IS NULL;
|
|
||||||
CREATE INDEX idx_clients_activity ON clients(activity_level, last_active_at DESC)
|
|
||||||
WHERE deleted_at IS NULL;
|
|
||||||
|
|
||||||
-- 客源跟进日志(复用结构,单独表避免与房源日志混合)
|
|
||||||
CREATE TABLE client_follow_logs (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
|
||||||
purpose VARCHAR(50),
|
|
||||||
content TEXT,
|
|
||||||
log_tag VARCHAR(50),
|
|
||||||
is_public BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
operator_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
|
||||||
operator_snapshot JSONB,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
deleted_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_client_logs_client ON client_follow_logs(client_id, created_at DESC)
|
|
||||||
WHERE deleted_at IS NULL;
|
|
||||||
|
|
||||||
-- 智能配房记录(客源 ↔ 房源 匹配)
|
|
||||||
CREATE TABLE client_property_matches (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
|
||||||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
|
||||||
match_score NUMERIC(5,2), -- 匹配度评分
|
|
||||||
match_reason JSONB, -- 匹配原因详情
|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'suggested'
|
|
||||||
CHECK (status IN ('suggested','shared','viewing','rejected')),
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX idx_client_property_match
|
|
||||||
ON client_property_matches(client_id, property_id);
|
|
||||||
|
|
||||||
-- 带看记录
|
|
||||||
CREATE TABLE viewings (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE RESTRICT,
|
|
||||||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE RESTRICT,
|
|
||||||
agent_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
|
||||||
|
|
||||||
viewing_type VARCHAR(20) NOT NULL DEFAULT 'first'
|
|
||||||
CHECK (viewing_type IN ('first','revisit','empty','interview')),
|
|
||||||
-- first=带看, revisit=复看, empty=空看, interview=面访
|
|
||||||
|
|
||||||
scheduled_at TIMESTAMPTZ,
|
|
||||||
completed_at TIMESTAMPTZ,
|
|
||||||
result VARCHAR(20)
|
|
||||||
CHECK (result IN ('interested','not_interested',
|
|
||||||
'negotiating','cancelled')),
|
|
||||||
remarks TEXT,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_viewings_property ON viewings(property_id);
|
|
||||||
CREATE INDEX idx_viewings_client ON viewings(client_id);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1290,7 +1115,7 @@ CREATE INDEX idx_number_holder_approvals_status ON number_holder_approvals(statu
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 四、关键索引汇总与查询优化策略
|
## 五、关键索引汇总与查询优化策略
|
||||||
|
|
||||||
### 4.1 房源列表页核心查询分析
|
### 4.1 房源列表页核心查询分析
|
||||||
|
|
||||||
@@ -1372,7 +1197,7 @@ CREATE TRIGGER trg_update_last_followed
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 五、Redis 缓存策略
|
## 六、Redis 缓存策略
|
||||||
|
|
||||||
### 5.1 缓存 Key 规范
|
### 5.1 缓存 Key 规范
|
||||||
|
|
||||||
@@ -1422,7 +1247,7 @@ CREATE TRIGGER trg_update_last_followed
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 六、Django Model 层设计要点
|
## 七、Django Model 层设计要点
|
||||||
|
|
||||||
### 6.1 抽象基类
|
### 6.1 抽象基类
|
||||||
|
|
||||||
@@ -1500,7 +1325,7 @@ class PropertyManager(ActiveManager):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 七、数据量与性能预测
|
## 八、数据量与性能预测
|
||||||
|
|
||||||
| 表名 | 预估行数 | 增长速度 | 分区策略 |
|
| 表名 | 预估行数 | 增长速度 | 分区策略 |
|
||||||
|------|---------|---------|---------|
|
|------|---------|---------|---------|
|
||||||
@@ -1514,7 +1339,7 @@ class PropertyManager(ActiveManager):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 八、必须在开发启动前明确的数据架构决策
|
## 九、必须在开发启动前明确的数据架构决策
|
||||||
|
|
||||||
| 决策项 | 推荐方案 | 风险 |
|
| 决策项 | 推荐方案 | 风险 |
|
||||||
|-------|---------|------|
|
|-------|---------|------|
|
||||||
|
|||||||
574
Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md
Normal file
574
Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md
Normal file
@@ -0,0 +1,574 @@
|
|||||||
|
# Fonrey — 客源管理数据模型(DATA_MODEL_CLIENT)
|
||||||
|
|
||||||
|
> **所属系统**: Fonrey 房产经纪管理系统
|
||||||
|
> **版本**: v1.0
|
||||||
|
> **日期**: 2026-04-24
|
||||||
|
> **关联模块**: `apps/client/` — 私客、公客、成交客、跟进记录、带看、智能配房
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、领域概览(Domain Overview)
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
- **Client(客源)**:有购房/租房意向或历史成交记录的客户。核心实体,与房源(Property)是系统业务闭环的两端。
|
||||||
|
- **客源类型**:
|
||||||
|
- **私客(private)**:经纪人独占跟进的意向客户,是本期核心。
|
||||||
|
- **公客(public)**:私客超时未跟进或手动转公后,进入全公司共享客源池。
|
||||||
|
- **成交客(transacted)**:已完成购房/租房成交的客户,用于复购/转介绍跟进。
|
||||||
|
- **ClientContact(联系人)**:一个客源可有多个联系人,每个联系人有独立手机号。手机号加密存储,用于重复检测(「私客与成交客重复」)。
|
||||||
|
- **ClientRequirement(需求信息)**:购房/租房的详细偏好。一个客源可同时有「二手」「新房」「租房」三种需求类型(分别对应独立的需求记录)。
|
||||||
|
- **ClientFollowLog(跟进日志)**:经纪人与客户每次沟通的书面记录,是客源活跃度计算的数据来源。
|
||||||
|
- **Viewing(带看记录)**:与 Property 模块共享此表,记录经纪人带客户看房的过程。见主 DATA_MODEL.md 3.17 节。
|
||||||
|
- **ClientPropertyMatch(智能配房)**:系统按需求自动匹配的房源列表,分「录客配房」和「系统配房」两种来源。
|
||||||
|
- **ClientFavoriteFolder(收藏夹)**:经纪人自定义的客源分组收藏夹。
|
||||||
|
|
||||||
|
### 关键业务规则
|
||||||
|
|
||||||
|
1. **私客手机号唯一性**:录入联系人手机号时,系统通过 `phone_hash` 检测是否与现有私客/成交客/公客重复,并在列表顶部提示重复数量。
|
||||||
|
2. **活跃度计算**:系统根据「最后跟进日期」自动计算客源活跃度,分为:新配偶(新建)/ 7日活跃 / 30日活跃 / 90日活跃 / 即将过期 / 无效。具体阈值由运营配置。
|
||||||
|
3. **私客自动转公规则**:超过配置天数(如 30 天)无跟进记录,系统自动将私客标记为公客(`transfer_to_public_type = 'auto'`)。
|
||||||
|
4. **状态机**:客源状态有严格流转规则(见第四章),不可跳过转台。
|
||||||
|
5. **跟进目的枚举**:由 `lookup_items` 表维护,运营可配置,当前已知 23 项(见 Story 8)。
|
||||||
|
6. **号码查看审计**:查看联系人明文号码需记录 `client_follow_logs`(`log_type = 'sensitive_view'`),不可删除。
|
||||||
|
7. **需求类型独立存储**:同一客源可同时有「二手购房」「租房」两类需求,分别存储在独立需求记录中,由 `client_requirements.requirement_type` 区分。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、实体关系
|
||||||
|
|
||||||
|
```
|
||||||
|
Client (客源主表)
|
||||||
|
│
|
||||||
|
├── 1:N ── ClientContact (联系人,多个号码)
|
||||||
|
├── 1:N ── ClientRequirement (需求信息,可多类型)
|
||||||
|
├── 1:N ── ClientFollowLog (跟进日志,高写入频率)
|
||||||
|
├── 1:N ── ClientViewing (带看预约)
|
||||||
|
├── 1:N ── ClientPropertyMatch (智能配房结果)
|
||||||
|
├── 1:1 ── ClientActivityCache (活跃度缓存,异步计算)
|
||||||
|
├── N:M ── ClientFavoriteFolder (通过 client_folder_items 关联)
|
||||||
|
└── 1:N ── ClientStatusLog (状态变更日志,不可删)
|
||||||
|
|
||||||
|
ClientFavoriteFolder
|
||||||
|
└── 1:N ── ClientFolderItem (收藏夹中的客源)
|
||||||
|
|
||||||
|
Staff (员工)
|
||||||
|
├── first_recorder_id → Client (首录人)
|
||||||
|
└── owner_id → Client (归属人)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Schema 定义
|
||||||
|
|
||||||
|
### 3.1 clients — 客源主表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| client_no | VARCHAR(30) | UNIQUE, NOT NULL | 系统生成的客源编号,格式由运营配置(如 KY20260424001) |
|
||||||
|
| client_type | VARCHAR(20) | NOT NULL DEFAULT 'private' | `private`=私客 / `public`=公客 / `transacted`=成交客 |
|
||||||
|
| status | VARCHAR(20) | NOT NULL DEFAULT 'buying' | 见下方枚举 |
|
||||||
|
| grade | VARCHAR(5) | NOT NULL DEFAULT 'C' | `A_urgent`=A急迫 / `A` / `B`=较强 / `C`=一般 / `D`=较弱 / `E`=暂不关注 |
|
||||||
|
| property_usage | VARCHAR(30) | NOT NULL DEFAULT 'residential' | `residential`=住宅 / `villa`=别墅 / `commercial_residential`=商住 / `shop`=商铺 / `office`=写字楼 / `other`=其他 |
|
||||||
|
| buying_purpose | VARCHAR(20)[] | | 购房目的多选:`rigid`=刚需 / `investment`=投资 / `school_district`=学区 / `upgrade`=改善 / `commercial`=商用 / `other`=其他 |
|
||||||
|
| payment_method | VARCHAR(30) | | `full`=全额 / `mortgage`=商业贷款 / `mortgage_fund`=商贷+公积金 / `fund`=公积金 |
|
||||||
|
| properties_owned | VARCHAR(20) | | `none`=无 / `local_none`=本地无外地有 / `local_has`=本地有 |
|
||||||
|
| has_loan_record | BOOLEAN | | 有无贷款记录 |
|
||||||
|
| id_type | VARCHAR(20) | | 证件类型:`id_card` / `passport` / `hk_macao` / `other` |
|
||||||
|
| id_number_enc | BYTEA | | 证件号码(AES 加密) |
|
||||||
|
| source | VARCHAR(50) | | 客户来源(lookup_items 维护) |
|
||||||
|
| remarks | TEXT | | 备注,最多200字 |
|
||||||
|
| is_starred | BOOLEAN | NOT NULL DEFAULT FALSE | 是否收藏(快速标记,详细收藏夹用 client_folder_items) |
|
||||||
|
| is_pinned | BOOLEAN | NOT NULL DEFAULT FALSE | 是否置顶(列表顶部置顶) |
|
||||||
|
| is_big_value | BOOLEAN | NOT NULL DEFAULT FALSE | 是否大价值客户(影响筛选展示) |
|
||||||
|
| is_protected | BOOLEAN | NOT NULL DEFAULT FALSE | 是否保护客(影响转公逻辑) |
|
||||||
|
| prefers_new_house | BOOLEAN | | 偏好新房(用于筛选) |
|
||||||
|
| transfer_to_public_type | VARCHAR(20) | | 转公客方式:`manual`=手动转公 / `auto`=自动转公(超时) / `marketing_jump`=营销客跳公 / `resource_public`=资料客素公 |
|
||||||
|
| transferred_public_at | TIMESTAMPTZ | | 进入公客池时间 |
|
||||||
|
| invalid_reason | VARCHAR(30) | | 无效原因:`invalid_phone`=号码无效 / `peer_agent`=同行 / `ad`=广告推销 / `no_intent`=无意向 / `other` |
|
||||||
|
| invalidated_at | TIMESTAMPTZ | | 标记无效时间 |
|
||||||
|
| transacted_at | DATE | | 成交日期 |
|
||||||
|
| transacted_property_id | UUID | FK→properties, SET NULL | 成交关联的房源 |
|
||||||
|
| transacted_price | NUMERIC(12,2) | | 成交价格(万元) |
|
||||||
|
| transacted_type | VARCHAR(20) | | 成交类型:`bought`=我购 / `rented`=我租 |
|
||||||
|
| transacted_property_type | VARCHAR(20) | | 成交房源类型:`second_hand`=二手 / `new_house`=新房 |
|
||||||
|
| first_recorder_id | UUID | FK→staff, SET NULL | 首录人 |
|
||||||
|
| owner_id | UUID | FK→staff, SET NULL | 归属人(私客独占跟进人) |
|
||||||
|
| org_unit_id | UUID | FK→org_units, SET NULL | 归属部门(冗余,加速筛选) |
|
||||||
|
| activity_level | VARCHAR(20) | | `new_matched`=新配偶 / `active_7d` / `active_30d` / `active_90d` / `expiring` / `frozen` / `invalid`(异步计算)|
|
||||||
|
| last_active_at | TIMESTAMPTZ | | 最后有效跟进时间(触发器维护) |
|
||||||
|
| last_follow_at | TIMESTAMPTZ | | 最后跟进时间(冗余,列表排序用) |
|
||||||
|
| commission_date | DATE | | 委托日期 |
|
||||||
|
| entrust_count | SMALLINT | NOT NULL DEFAULT 1 | 委托次数(成交后再委托则累加) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除 |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | |
|
||||||
|
| updated_by | UUID | FK→staff, SET NULL | |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_clients_client_no ON clients(client_no) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_type_status ON clients(client_type, status) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_owner ON clients(owner_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_org_unit ON clients(org_unit_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_activity ON clients(activity_level, last_active_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_grade ON clients(grade) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_clients_transferred_at ON clients(transferred_public_at DESC) WHERE client_type = 'public';
|
||||||
|
CREATE INDEX idx_clients_last_follow ON clients(last_follow_at DESC NULLS LAST) WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 client_contacts — 联系人表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
|
||||||
|
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | 联系人1为主联系人(sort_order=0) |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 联系人姓名 |
|
||||||
|
| gender | VARCHAR(10) | NOT NULL DEFAULT 'male' | `male`=先生 / `female`=女士 |
|
||||||
|
| phone_enc | BYTEA | NOT NULL | AES-256-GCM 加密手机号(电话1) |
|
||||||
|
| phone_hash | VARCHAR(64) | NOT NULL | SHA-256 哈希(重复检测) |
|
||||||
|
| phone_country_code | VARCHAR(10) | NOT NULL DEFAULT '+86' | 国际区号 |
|
||||||
|
| phone_is_invalid | BOOLEAN | NOT NULL DEFAULT FALSE | 是否被标记为无效号码 |
|
||||||
|
| phone2_enc | BYTEA | | 备用电话2 |
|
||||||
|
| phone2_hash | VARCHAR(64) | | |
|
||||||
|
| wechat | VARCHAR(100) | | 微信号 |
|
||||||
|
| qq | VARCHAR(20) | | QQ号 |
|
||||||
|
| remarks | VARCHAR(200) | | 联系人备注,最多200字 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除(不影响客源本身) |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
-- 关键:手机号哈希全局唯一索引(用于重复客源检测)
|
||||||
|
CREATE INDEX idx_client_contacts_phone_hash ON client_contacts(phone_hash) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_contacts_phone2_hash ON client_contacts(phone2_hash) WHERE phone2_hash IS NOT NULL AND deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_contacts_client ON client_contacts(client_id) WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务注意**:
|
||||||
|
- `sort_order = 0` 的联系人为主联系人,姓名用于客源姓名显示
|
||||||
|
- 手机号标记无效(`phone_is_invalid = TRUE`)时,不影响记录存在,但该号码不再参与重复检测
|
||||||
|
- 联系人软删除后客源仍保留,但若所有联系人均被删则客源实际上无有效号码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 client_requirements — 需求信息表
|
||||||
|
|
||||||
|
一个客源可同时有多类需求(二手购房、新房、租房),每类需求独立一条记录。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
|
||||||
|
| requirement_type | VARCHAR(20) | NOT NULL | `second_hand`=二手 / `new_house`=新房 / `rental`=租房 |
|
||||||
|
| is_primary | BOOLEAN | NOT NULL DEFAULT TRUE | 是否为主需求(用于列表展示) |
|
||||||
|
| budget_min | NUMERIC(12,2) | | 最低预算(万元/元,依据需求类型) |
|
||||||
|
| budget_max | NUMERIC(12,2) | | 最高预算 |
|
||||||
|
| area_min | NUMERIC(8,2) | | 最小面积(㎡) |
|
||||||
|
| area_max | NUMERIC(8,2) | | 最大面积 |
|
||||||
|
| bedroom_counts | SMALLINT[] | | 可接受卧室数:如 [2,3](多选) |
|
||||||
|
| floor_preferences | VARCHAR(20)[] | | 楼层偏好多选:`no_first`=不要一层 / `low`=低楼层 / `mid`=中楼层 / `high`=高楼层 / `no_top`=不要顶层 |
|
||||||
|
| orientations | VARCHAR(10)[] | | 朝向多选:`east`/`south`/`west`/`north` |
|
||||||
|
| decorations | VARCHAR(10)[] | | 装修偏好多选(枚举同 properties.decoration) |
|
||||||
|
| building_age_ranges | VARCHAR(20)[] | | 楼龄多选:`within_5y`/`5_10y`/`10_15y`/`15_20y`/`over_20y` |
|
||||||
|
| intent_district_ids | UUID[] | | 意向行政区 ID 数组 |
|
||||||
|
| intent_business_area_ids | UUID[] | | 意向商圈 ID 数组 |
|
||||||
|
| intent_complex_names | TEXT | | 意向小区(文本,逗号分隔,最多500字) |
|
||||||
|
| transportation | VARCHAR(50) | | 交通要求(最多50字) |
|
||||||
|
| intent_school_names | TEXT | | 意向学校(文本,逗号分隔) |
|
||||||
|
| school_enrollment_date | DATE | | 入学时间(月份精度,取该月1日存储) |
|
||||||
|
| traffic_preference | TEXT | | 交通备注 |
|
||||||
|
| requirement_notes | VARCHAR(200) | | 需求备注(最多200字) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_client_requirements_client ON client_requirements(client_id);
|
||||||
|
CREATE INDEX idx_client_requirements_type ON client_requirements(requirement_type, client_id);
|
||||||
|
-- 智能配房时按预算/面积范围查询
|
||||||
|
CREATE INDEX idx_client_requirements_budget ON client_requirements(budget_min, budget_max);
|
||||||
|
CREATE INDEX idx_client_requirements_area ON client_requirements(area_min, area_max);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 client_follow_logs — 客源跟进日志
|
||||||
|
|
||||||
|
> 与 `follow_logs`(房源跟进)结构类似,独立存储以避免跨模块混淆。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
|
||||||
|
| log_type | VARCHAR(30) | NOT NULL | 见下方枚举 |
|
||||||
|
| purpose | VARCHAR(50) | | 跟进目的(lookup_items 维护,23项) |
|
||||||
|
| content | TEXT | | 跟进内容(最少6字,最多500字) |
|
||||||
|
| log_tag | VARCHAR(50) | | 跟进标签:`has_recording`=有录音 / `has_photo`=有图片 / `not_satisfied`=对房源不满意 / `still_considering`=还在考虑 / `ready_to_deposit`=可交定金 |
|
||||||
|
| change_detail | JSONB | | 修改跟进专用,格式:`{"field": "grade", "old": "C", "new": "B", "label": "等级"}` |
|
||||||
|
| is_public | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=仅本人及上级可见 |
|
||||||
|
| is_deletable | BOOLEAN | NOT NULL DEFAULT TRUE | 敏感信息查看类型为 FALSE,不可删除 |
|
||||||
|
| operator_id | UUID | FK→staff, SET NULL | 操作人 |
|
||||||
|
| operator_snapshot | JSONB | | `{name, store_group, role}`(防止人员调动后显示异常) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 仅 is_deletable=TRUE 时可软删 |
|
||||||
|
|
||||||
|
**log_type 枚举**:
|
||||||
|
```
|
||||||
|
written = 写入跟进(经纪人主动写)
|
||||||
|
modified = 修改跟进(字段变更自动生成)
|
||||||
|
sensitive_view= 敏感信息查看(查看号码等,不可删)
|
||||||
|
other = 其他跟进(系统自动:新增私客/状态变更等)
|
||||||
|
system = 系统日志
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_client_follow_logs_client_time ON client_follow_logs(client_id, created_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_follow_logs_type ON client_follow_logs(client_id, log_type, created_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_follow_logs_operator ON client_follow_logs(operator_id, created_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
-- 不可删记录(合规审计)
|
||||||
|
CREATE INDEX idx_client_follow_sensitive ON client_follow_logs(client_id, created_at DESC) WHERE log_type = 'sensitive_view';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 client_follow_log_attachments — 跟进附件
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| follow_log_id | UUID | NOT NULL, FK→client_follow_logs, CASCADE | |
|
||||||
|
| file_key | TEXT | NOT NULL | R2/S3 存储路径 |
|
||||||
|
| file_name | VARCHAR(255) | NOT NULL | |
|
||||||
|
| file_size | INTEGER | NOT NULL | bytes,最大 20MB |
|
||||||
|
| file_type | VARCHAR(10) | CHECK | `bmp`/`jpg`/`png`/`gif` |
|
||||||
|
| has_location | BOOLEAN | NOT NULL DEFAULT FALSE | 是否含 GPS 位置信息 |
|
||||||
|
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 client_viewings — 带看记录(客源侧视图)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, RESTRICT | |
|
||||||
|
| property_id | UUID | NOT NULL, FK→properties, RESTRICT | |
|
||||||
|
| viewing_type | VARCHAR(20) | NOT NULL DEFAULT 'viewing' | `appointment`=预约 / `viewing`=带看 / `revisit`=复看 / `empty`=空看 |
|
||||||
|
| agent_id | UUID | FK→staff, SET NULL | 主带看经纪人 |
|
||||||
|
| companion_ids | UUID[] | | 陪看人员 ID 数组(最多5人) |
|
||||||
|
| cooperator_ids | UUID[] | | 合作带看人 ID 数组(最多5人) |
|
||||||
|
| scheduled_at | TIMESTAMPTZ | | 预约时间 |
|
||||||
|
| viewing_start_at | TIMESTAMPTZ | | 实际带看开始时间 |
|
||||||
|
| viewing_end_at | TIMESTAMPTZ | | 结束时间 |
|
||||||
|
| situation | TEXT | | 带看情况(必填,≥6字) |
|
||||||
|
| client_intent | VARCHAR(20) | | 客户意向:`interested`=感兴趣 / `not_interested`=不感兴趣 / `negotiating`=谈判中 / `cancelled`=取消 |
|
||||||
|
| viewing_progress | SMALLINT | | 带看进度(1=一看,2=二看...,冗余字段,触发器维护) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_client_viewings_client ON client_viewings(client_id, viewing_start_at DESC) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_viewings_property ON client_viewings(property_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_client_viewings_agent ON client_viewings(agent_id) WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 client_property_matches — 智能配房
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
|
||||||
|
| property_id | UUID | NOT NULL, FK→properties, CASCADE | |
|
||||||
|
| match_source | VARCHAR(20) | NOT NULL DEFAULT 'recorded' | `recorded`=录客配房(基于录入需求) / `system`=系统配房(算法推荐) |
|
||||||
|
| match_group | VARCHAR(30) | | 分组:`quality_layout`=优质户型 / `price_reduced`=降价 / `hot`=热门 / `newly_listed`=新上 |
|
||||||
|
| match_score | NUMERIC(5,2) | | 匹配度评分(0-100) |
|
||||||
|
| match_reasons | JSONB | | 匹配原因详情,格式:`[{"key": "budget", "match": true}, ...]` |
|
||||||
|
| status | VARCHAR(20) | NOT NULL DEFAULT 'suggested' | `suggested`=待推送 / `shared`=已分享 / `rejected`=已反馈不合适 / `viewed`=客户已查看 |
|
||||||
|
| shared_at | TIMESTAMPTZ | | 分享时间 |
|
||||||
|
| feedback | VARCHAR(50) | | 反馈原因(lookup_items 维护) |
|
||||||
|
| calculated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 配房计算时间 |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_client_matches_pair ON client_property_matches(client_id, property_id);
|
||||||
|
CREATE INDEX idx_client_matches_client ON client_property_matches(client_id, match_source, match_group);
|
||||||
|
CREATE INDEX idx_client_matches_status ON client_property_matches(client_id, status) WHERE status != 'rejected';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.8 client_status_logs — 状态变更日志(不可删)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, RESTRICT | |
|
||||||
|
| change_type | VARCHAR(30) | NOT NULL | `status_change`=改状态 / `grade_change`=改等级 / `to_public`=转公客 / `to_transacted`=转成交 / `to_invalid`=转无效 / `owner_change`=改归属人 / `source_change`=改来源 |
|
||||||
|
| old_value | JSONB | | 变更前快照,格式:`{"status": "buying", "label": "求购"}` |
|
||||||
|
| new_value | JSONB | | 变更后快照 |
|
||||||
|
| reason | TEXT | | 变更理由(改状态必填,最多200字) |
|
||||||
|
| operator_id | UUID | NOT NULL, FK→staff, RESTRICT | |
|
||||||
|
| operated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| ⚠️ 无 deleted_at | — | — | 此表记录**不可删除** |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_client_status_logs_client ON client_status_logs(client_id, operated_at DESC);
|
||||||
|
CREATE INDEX idx_client_status_logs_type ON client_status_logs(change_type, operated_at DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.9 client_favorite_folders — 私客收藏夹
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| staff_id | UUID | NOT NULL, FK→staff, CASCADE | 收藏夹所属经纪人 |
|
||||||
|
| name | VARCHAR(10) | NOT NULL | 收藏夹名称,最多10字 |
|
||||||
|
| is_default | BOOLEAN | NOT NULL DEFAULT FALSE | 系统默认收藏夹 |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_favorite_folders_staff ON client_favorite_folders(staff_id) WHERE deleted_at IS NULL;
|
||||||
|
-- 每个经纪人只能有一个默认收藏夹
|
||||||
|
CREATE UNIQUE INDEX idx_favorite_folders_default ON client_favorite_folders(staff_id) WHERE is_default = TRUE AND deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.10 client_folder_items — 收藏夹中的客源
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| folder_id | UUID | NOT NULL, FK→client_favorite_folders, CASCADE | |
|
||||||
|
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
|
||||||
|
| added_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| PRIMARY KEY | (folder_id, client_id) | | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_folder_items_client ON client_folder_items(client_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.11 client_school_preferences — 意向学校(多对多)
|
||||||
|
|
||||||
|
> 单独拆表便于学校搜索,避免文本字段模糊查询。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| requirement_id | UUID | NOT NULL, FK→client_requirements, CASCADE | |
|
||||||
|
| school_id | UUID | FK→schools, SET NULL | 从学校表选择,允许为 NULL(自由输入) |
|
||||||
|
| school_name | VARCHAR(100) | NOT NULL | 学校名称(当 school_id 为 NULL 时为手动输入) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_school_prefs_requirement ON client_school_preferences(requirement_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、枚举常量
|
||||||
|
|
||||||
|
### clients.status(客源状态)
|
||||||
|
|
||||||
|
```
|
||||||
|
buying = 求购(私客活跃态)
|
||||||
|
renting = 求租(私客活跃态)
|
||||||
|
buy_or_rent = 租购(私客活跃态)
|
||||||
|
suspended = 暂缓(暂时无需求,不计入活跃统计)
|
||||||
|
bought = 已购(成交客:我购)
|
||||||
|
rented_done = 已租(成交客:我租)
|
||||||
|
public = 公客(已转入公客池)
|
||||||
|
invalid = 无效(号码无效/无意向等)
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态流转规则**:
|
||||||
|
```
|
||||||
|
buying/renting/buy_or_rent
|
||||||
|
→ suspended (改状态操作,可逆)
|
||||||
|
→ public (手动转公 or 超时自动转公,不可逆)
|
||||||
|
→ bought/rented_done (转成交,不可逆)
|
||||||
|
→ invalid (转无效,需经理审批后可恢复)
|
||||||
|
```
|
||||||
|
|
||||||
|
### clients.grade(等级)
|
||||||
|
|
||||||
|
```
|
||||||
|
A_urgent = A(急迫)
|
||||||
|
A = A
|
||||||
|
B = B(较强)
|
||||||
|
C = C(一般,默认值)
|
||||||
|
D = D(较弱)
|
||||||
|
E = E(暂不关注)
|
||||||
|
```
|
||||||
|
|
||||||
|
### client_status_logs.change_type(变更类型)
|
||||||
|
|
||||||
|
```
|
||||||
|
status_change = 改状态(含改等级时同时改状态的情况)
|
||||||
|
grade_change = 改等级
|
||||||
|
to_public = 转公客(manual=手动 or auto=自动)
|
||||||
|
to_transacted = 转成交(记录成交信息)
|
||||||
|
to_invalid = 转无效(含无效原因)
|
||||||
|
owner_change = 改归属人
|
||||||
|
source_change = 改来源
|
||||||
|
merge = 合并客源(被合并的记录保留日志)
|
||||||
|
```
|
||||||
|
|
||||||
|
### clients.activity_level(活跃度分层,系统计算)
|
||||||
|
|
||||||
|
| 值 | 含义 | 触发条件(示例,以运营配置为准) |
|
||||||
|
|----|------|------|
|
||||||
|
| `new_matched` | 新配偶 | 录入后 3 天内 |
|
||||||
|
| `active_7d` | 7日活跃 | 最后跟进在 7 天内 |
|
||||||
|
| `active_30d` | 30日活跃 | 最后跟进在 30 天内 |
|
||||||
|
| `active_90d` | 90日活跃 | 最后跟进在 90 天内 |
|
||||||
|
| `expiring` | 即将过期 | 距自动转公还有 N 天 |
|
||||||
|
| `frozen` | 冻结(暂缓) | status = suspended |
|
||||||
|
| `invalid` | 无效 | status = invalid |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、查询模式
|
||||||
|
|
||||||
|
### 5.1 私客列表页(求购 Tab)核心查询
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 典型:当前经纪人名下 + 求购状态 + 等级筛选 + 按最后跟进排序
|
||||||
|
SELECT c.id, c.status, c.grade, c.activity_level,
|
||||||
|
c.last_follow_at, c.commission_date, c.buying_purpose,
|
||||||
|
cc.name AS contact_name, -- JOIN 主联系人
|
||||||
|
s.name AS owner_name, ou.name AS org_unit_name,
|
||||||
|
COUNT(cpm.id) AS match_count -- 智能配房数量
|
||||||
|
FROM clients c
|
||||||
|
JOIN client_contacts cc ON cc.client_id = c.id AND cc.sort_order = 0 AND cc.deleted_at IS NULL
|
||||||
|
JOIN staff s ON s.id = c.owner_id
|
||||||
|
JOIN org_units ou ON ou.id = c.org_unit_id
|
||||||
|
LEFT JOIN client_property_matches cpm ON cpm.client_id = c.id AND cpm.status != 'rejected'
|
||||||
|
WHERE c.client_type = 'private'
|
||||||
|
AND c.owner_id = :current_staff_id -- 与我相关
|
||||||
|
AND c.status IN ('buying', 'buy_or_rent')
|
||||||
|
AND c.deleted_at IS NULL
|
||||||
|
GROUP BY c.id, cc.name, s.name, ou.name
|
||||||
|
ORDER BY c.last_follow_at DESC NULLS LAST
|
||||||
|
LIMIT 20 OFFSET :offset;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 重复客源检测(录入/编辑时触发)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 手机号哈希碰撞检测(私客、成交客、公客三池同时检查)
|
||||||
|
SELECT c.id, c.client_type, c.status, c.client_no,
|
||||||
|
cc.name AS contact_name
|
||||||
|
FROM client_contacts cc
|
||||||
|
JOIN clients c ON cc.client_id = c.id
|
||||||
|
WHERE cc.phone_hash = :new_phone_hash
|
||||||
|
AND cc.deleted_at IS NULL
|
||||||
|
AND c.deleted_at IS NULL
|
||||||
|
AND c.status != 'invalid';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 活跃度批量更新(Celery 定时任务,每日凌晨执行)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 更新活跃度(以7日活跃为例)
|
||||||
|
UPDATE clients
|
||||||
|
SET activity_level = 'active_7d',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE client_type = 'private'
|
||||||
|
AND status NOT IN ('invalid', 'public', 'bought', 'rented_done')
|
||||||
|
AND last_follow_at >= NOW() - INTERVAL '7 days'
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 私客自动转公(超时无跟进,Celery 定时任务)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 查询应自动转公的私客(阈值由运营配置,假设30天)
|
||||||
|
SELECT id FROM clients
|
||||||
|
WHERE client_type = 'private'
|
||||||
|
AND status IN ('buying', 'renting', 'buy_or_rent')
|
||||||
|
AND last_follow_at < NOW() - INTERVAL '30 days'
|
||||||
|
AND is_protected = FALSE
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
-- 后续在 Application 层批量更新 client_type='public', transfer_to_public_type='auto'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、触发器
|
||||||
|
|
||||||
|
### 6.1 last_follow_at 自动维护
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 每次写入跟进日志时,自动更新 clients.last_follow_at
|
||||||
|
CREATE OR REPLACE FUNCTION update_client_last_follow()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.log_type = 'written' THEN
|
||||||
|
UPDATE clients
|
||||||
|
SET last_follow_at = NEW.created_at,
|
||||||
|
last_active_at = NEW.created_at,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = NEW.client_id;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_client_last_follow
|
||||||
|
AFTER INSERT ON client_follow_logs
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_client_last_follow();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 viewing_progress 自动维护
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 每次新增带看记录时,自动更新 clients 的带看进度冗余字段
|
||||||
|
CREATE OR REPLACE FUNCTION update_client_viewing_progress()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE clients
|
||||||
|
SET updated_at = NOW()
|
||||||
|
WHERE id = NEW.client_id;
|
||||||
|
-- Application 层根据 COUNT(viewings) 计算具体进度
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_client_viewing_progress
|
||||||
|
AFTER INSERT ON client_viewings
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_client_viewing_progress();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、禁止操作
|
||||||
|
|
||||||
|
- ❌ **严禁硬删除 clients 记录**:无效/转公客/成交客均通过 status 和 soft delete 处理,历史跟进/带看依赖外键
|
||||||
|
- ❌ **严禁删除 client_status_logs**:状态变更为不可变审计日志
|
||||||
|
- ❌ **严禁删除 log_type='sensitive_view' 的跟进记录**:必须通过 `is_deletable=FALSE` 约束在应用层拦截
|
||||||
|
- ❌ **严禁明文存储联系人手机号**:必须走 `EncryptedPhoneField`,`phone_hash` 用于索引和重复检测
|
||||||
|
- ❌ **严禁跳过状态机流转**:如私客不可直接跳过「求购」变为「无效」而不生成 status log
|
||||||
|
- ❌ **严禁在没有 `client_type` 过滤的情况下查询客源列表**:私客/公客/成交客数据量均较大,必须按类型隔离查询
|
||||||
|
- ❌ **严禁查询 clients 时不带 `deleted_at IS NULL`**:软删除过滤必须存在
|
||||||
547
Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md
Normal file
547
Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
# Fonrey — 楼盘与区域数据模型(DATA_MODEL_COMPLEX)
|
||||||
|
|
||||||
|
> **所属系统**: Fonrey 房产经纪管理系统
|
||||||
|
> **版本**: v1.0
|
||||||
|
> **日期**: 2026-04-24
|
||||||
|
> **关联模块**: `apps/complex/` — 楼盘/小区、楼栋、结构(楼层+房号)、区域、学校
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、领域概览(Domain Overview)
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
- **Complex(楼盘/小区)**:房源录入的基础底座。每套房源必须归属于某一楼盘。楼盘数据由运营/数据管理员集中维护,质量直接影响房源录入效率和搜索精度。
|
||||||
|
- **Building(楼栋/单元)**:楼盘下的物理楼栋,是组织房源位置的第二级。一个楼盘可有多个楼栋(如「1号楼」「2栋2单元」)。
|
||||||
|
- **RoomUnit(房号/结构单元)**:楼栋内特定楼层的某个房间标识,是房源定位的最细粒度。支持「标准结构」(经运营标准化)和「非标结构」(未归一化)两类。
|
||||||
|
- **District(城区/行政区)**:行政区划,如静安区、闵行区。
|
||||||
|
- **BusinessArea(商圈/板块)**:商圈是区域内的细分市场区域,如「南京西路商圈」,一个楼盘可跨多个商圈。
|
||||||
|
- **School(学校)**:楼盘对口学校,是买家购房决策的核心关注点。一个楼盘可关联多所学校,一所学校可对口多个楼盘。
|
||||||
|
- **MetroLine / MetroStation(地铁线路/站点)**:楼盘与最近地铁站的距离关系,用于通勤筛选。
|
||||||
|
|
||||||
|
### 关键业务规则
|
||||||
|
|
||||||
|
1. **楼盘名称不可在编辑页修改**:楼盘名称(`name`)变更须通过「合并楼盘」或「申请流程」处理,防止经纪人随意改名造成数据混乱。
|
||||||
|
2. **数据锁定机制**:楼盘有 4 类锁(楼栋锁/房号锁/信息锁/标准房号锁),锁定后对应数据只有管理员可解锁修改。
|
||||||
|
3. **非标结构处理**:未与标准结构关联的房号为「非标」,系统记录非标数量,引导运营逐步消除。
|
||||||
|
4. **搜索依赖全文检索**:楼盘名称、别名、地址需维护 `search_vector`(`tsvector`)以支持模糊搜索和联想补全。
|
||||||
|
5. **地理坐标优先级**:楼盘坐标是区域聚合展示(地图找房)的核心数据,完整度目标 ≥ 90%。
|
||||||
|
6. **学校关联影响房源**:从楼盘详情删除对口学校,会级联删除该楼盘下所有房源的对应学区标注。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、实体关系
|
||||||
|
|
||||||
|
```
|
||||||
|
District (城区/行政区)
|
||||||
|
└── 1:N ── BusinessArea (商圈/板块)
|
||||||
|
└── N:M ── Complex (through complex_business_areas)
|
||||||
|
|
||||||
|
Complex (楼盘)
|
||||||
|
├── N:M ── BusinessArea (through complex_business_areas)
|
||||||
|
├── N:M ── School (through complex_schools)
|
||||||
|
├── N:M ── MetroStation (through complex_metro_stations, 附带距离)
|
||||||
|
├── 1:N ── Building (楼栋/单元)
|
||||||
|
│ └── 1:N ── RoomUnit (楼层+房号)
|
||||||
|
├── 1:N ── ComplexPhoto (楼盘照片:楼盘图/户型图/VR)
|
||||||
|
├── 1:N ── ComplexAttachment(附件)
|
||||||
|
├── 1:N ── ComplexPriceTrend(价格走势,月度)
|
||||||
|
└── 1:N ── ComplexAlias (别名)
|
||||||
|
|
||||||
|
MetroLine (地铁线路)
|
||||||
|
└── 1:N ── MetroStation (站点)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Schema 定义
|
||||||
|
|
||||||
|
### 3.1 districts — 城区/行政区
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| city | VARCHAR(50) | NOT NULL | 所属城市(支持多城市扩展,如「上海」「北京」) |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 行政区名称,如「静安区」 |
|
||||||
|
| short_name | VARCHAR(20) | | 简称,如「静安」 |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 列表展示排序 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用(不在筛选项中展示) |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_districts_city_name ON districts(city, name) WHERE is_active = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 business_areas — 商圈/板块
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| district_id | UUID | NOT NULL, FK→districts, RESTRICT | 所属城区 |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 商圈名称 |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | |
|
||||||
|
| latitude | NUMERIC(10,7) | | 商圈中心坐标(纬度) |
|
||||||
|
| longitude | NUMERIC(10,7) | | 商圈中心坐标(经度) |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_business_areas_district ON business_areas(district_id) WHERE is_active = TRUE;
|
||||||
|
CREATE UNIQUE INDEX idx_business_areas_name ON business_areas(district_id, name);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 metro_lines — 地铁线路
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| city | VARCHAR(50) | NOT NULL | 所属城市 |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 线路名,如「1号线」 |
|
||||||
|
| color | VARCHAR(7) | | 线路颜色 HEX(如 `#E3002B`) |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 metro_stations — 地铁站点
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| metro_line_id | UUID | NOT NULL, FK→metro_lines, CASCADE | 所属线路 |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 站名 |
|
||||||
|
| latitude | NUMERIC(10,7) | | 站点坐标 |
|
||||||
|
| longitude | NUMERIC(10,7) | | |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 沿线排序 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_metro_stations_line ON metro_stations(metro_line_id) WHERE is_active = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 schools — 学校
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| district_id | UUID | FK→districts, SET NULL | 所属城区 |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 学校名称 |
|
||||||
|
| type | VARCHAR(20) | | 学校类型:`primary`=小学 / `middle`=初中 / `high`=高中 / `k9`=九年一贯制 / `k12`=十二年一贯制 |
|
||||||
|
| nature | VARCHAR(20) | | 学校性质:`public`=公立 / `private`=私立 / `international`=国际学校 |
|
||||||
|
| level | VARCHAR(20) | | 学校等级:`normal`=普通 / `key`=重点 / `top`=名校 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_schools_district ON schools(district_id) WHERE is_active = TRUE;
|
||||||
|
CREATE INDEX idx_schools_name_trgm ON schools USING gin(name gin_trgm_ops);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 complexes — 楼盘/小区(核心基础表)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| name | VARCHAR(200) | NOT NULL | 标准楼盘名称,**不可在编辑页修改**(需走合并/申请流程) |
|
||||||
|
| district_id | UUID | FK→districts, SET NULL | 所属城区 |
|
||||||
|
| address | VARCHAR(500) | | 详细地址(不可在编辑页修改,需走纠错流程) |
|
||||||
|
| address_summary | VARCHAR(100) | | 概要地址(如「海波路1000弄」,可编辑) |
|
||||||
|
| latitude | NUMERIC(10,7) | | 楼盘坐标(纬度),完整度目标 ≥ 90% |
|
||||||
|
| longitude | NUMERIC(10,7) | | |
|
||||||
|
| **物业属性** | | | |
|
||||||
|
| property_usage_types | VARCHAR(20)[] | | 物业类型多选:`residential`/`villa`/`commercial_residential`/`commercial`/`office`/`other` |
|
||||||
|
| building_structure | VARCHAR(30) | | 楼栋结构枚举(运营维护):`unit_room`=单元-房号 / `other`=其他 |
|
||||||
|
| building_type | VARCHAR(20) | | 建筑类型:`slab`=板楼 / `tower`=塔楼 / `slab_tower`=板塔结合 |
|
||||||
|
| land_use_years | VARCHAR(30) | | 土地使用年限,如「70年」 |
|
||||||
|
| built_year | SMALLINT | | 竣工年份(可多选,存最早竣工年) |
|
||||||
|
| built_years | SMALLINT[] | | 竣工年份多值(楼盘分期竣工) |
|
||||||
|
| ownership_category | VARCHAR(30)[] | | 权属类别多选(运营维护枚举) |
|
||||||
|
| total_units | INTEGER | | 单元总数 |
|
||||||
|
| total_households | INTEGER | | 总户数 |
|
||||||
|
| **建设信息** | | | |
|
||||||
|
| total_floor_area | NUMERIC(12,2) | | 小区总建筑面积(m²) |
|
||||||
|
| plot_area | NUMERIC(12,2) | | 小区占地面积(m²) |
|
||||||
|
| plot_ratio | NUMERIC(5,2) | | 容积率 |
|
||||||
|
| green_rate | NUMERIC(5,2) | | 绿化率(%) |
|
||||||
|
| developer | VARCHAR(200) | | 开发商 |
|
||||||
|
| **物业信息** | | | |
|
||||||
|
| property_company | VARCHAR(200) | | 物业公司 |
|
||||||
|
| property_fee | NUMERIC(8,2) | | 物业费(元/m²/月) |
|
||||||
|
| property_phone | VARCHAR(30) | | 物业电话 |
|
||||||
|
| **停车** | | | |
|
||||||
|
| parking_total | INTEGER | | 车位总数 |
|
||||||
|
| parking_underground | INTEGER | | 地下车位数 |
|
||||||
|
| parking_ratio | VARCHAR(20) | | 停车位配比,如「100:63」 |
|
||||||
|
| **配套** | | | |
|
||||||
|
| water_type | VARCHAR(10) | | `civil`=民水 / `commercial`=商水 |
|
||||||
|
| electricity_type | VARCHAR(10) | | `civil`=民电 / `commercial`=商电 |
|
||||||
|
| has_central_heating | BOOLEAN | | 是否统一供暖 |
|
||||||
|
| has_gas | BOOLEAN | | 是否有燃气 |
|
||||||
|
| remarks | TEXT | | 备注 |
|
||||||
|
| **锁定状态** | | | |
|
||||||
|
| lock_building | BOOLEAN | NOT NULL DEFAULT FALSE | 楼栋锁(锁定后不可增删楼栋) |
|
||||||
|
| lock_room | BOOLEAN | NOT NULL DEFAULT FALSE | 房号锁 |
|
||||||
|
| lock_info | BOOLEAN | NOT NULL DEFAULT FALSE | 信息锁(锁定后基本信息只读) |
|
||||||
|
| lock_standard_room | BOOLEAN | NOT NULL DEFAULT FALSE | 标准房号锁 |
|
||||||
|
| **全文检索** | | | |
|
||||||
|
| search_vector | TSVECTOR | | 由触发器自动维护(name + alias + address) |
|
||||||
|
| **状态** | | | |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用楼盘 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除 |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | |
|
||||||
|
| updated_by | UUID | FK→staff, SET NULL | |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_complexes_district ON complexes(district_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_complexes_name_trgm ON complexes USING gin(name gin_trgm_ops); -- 模糊搜索
|
||||||
|
CREATE INDEX idx_complexes_search ON complexes USING gin(search_vector); -- 全文搜索
|
||||||
|
CREATE INDEX idx_complexes_geo ON complexes(latitude, longitude) WHERE deleted_at IS NULL AND latitude IS NOT NULL;
|
||||||
|
CREATE INDEX idx_complexes_active ON complexes(is_active) WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 complex_aliases — 楼盘别名
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||||
|
| alias | VARCHAR(200) | NOT NULL | 别名(最多20字/条,多别名多行存储) |
|
||||||
|
| is_system | BOOLEAN | NOT NULL DEFAULT FALSE | TRUE=系统/标准别名(只读),FALSE=用户自定义 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_complex_aliases_complex ON complex_aliases(complex_id);
|
||||||
|
CREATE INDEX idx_complex_aliases_alias_trgm ON complex_aliases USING gin(alias gin_trgm_ops);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.8 complex_business_areas — 楼盘与商圈多对多
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||||
|
| business_area_id | UUID | NOT NULL, FK→business_areas, CASCADE | |
|
||||||
|
| is_primary | BOOLEAN | NOT NULL DEFAULT FALSE | 主商圈(唯一)用于列表显示 |
|
||||||
|
| PRIMARY KEY | (complex_id, business_area_id) | | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 主商圈只能有一个
|
||||||
|
CREATE UNIQUE INDEX idx_complex_biz_area_primary ON complex_business_areas(complex_id) WHERE is_primary = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.9 complex_schools — 楼盘与学校关联
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||||
|
| school_id | UUID | NOT NULL, FK→schools, CASCADE | |
|
||||||
|
| zone_type | VARCHAR(30) | | 学区类型:`guaranteed`=对口 / `reference`=参考 / `lottery`=摇号 |
|
||||||
|
| PRIMARY KEY | (complex_id, school_id) | | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_complex_schools_school ON complex_schools(school_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务注意**:删除此关联记录时,需同步清理对应房源的学区标注(Application 层事务处理)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.10 complex_metro_stations — 楼盘与地铁站关联
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||||
|
| station_id | UUID | NOT NULL, FK→metro_stations, CASCADE | |
|
||||||
|
| distance_meters | INTEGER | | 步行距离(米) |
|
||||||
|
| PRIMARY KEY | (complex_id, station_id) | | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_complex_metro_complex ON complex_metro_stations(complex_id);
|
||||||
|
CREATE INDEX idx_complex_metro_station ON complex_metro_stations(station_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.11 buildings — 楼栋/单元
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 楼栋名,如「1号楼」「A栋2单元」 |
|
||||||
|
| is_standard | BOOLEAN | NOT NULL DEFAULT FALSE | TRUE=标准结构(经运营核准) |
|
||||||
|
| property_usage_type | VARCHAR(20) | | 物业类型(可与楼盘不同,如商住楼盘内有纯商铺楼栋) |
|
||||||
|
| built_year | SMALLINT | | 竣工年份 |
|
||||||
|
| total_floors | SMALLINT | | 总层数 |
|
||||||
|
| land_use_years | VARCHAR(30) | | 土地使用年限 |
|
||||||
|
| has_elevator | BOOLEAN | | 是否有电梯 |
|
||||||
|
| school_id | UUID | FK→schools, SET NULL | 关联对口学校(楼栋级别的学区差异) |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_buildings_complex ON buildings(complex_id) WHERE is_active = TRUE;
|
||||||
|
CREATE UNIQUE INDEX idx_buildings_name ON buildings(complex_id, name) WHERE is_active = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.12 room_units — 房号/结构单元(楼层+房间号)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| building_id | UUID | NOT NULL, FK→buildings, CASCADE | |
|
||||||
|
| floor | SMALLINT | NOT NULL | 楼层(实际层数,地下为负数) |
|
||||||
|
| floor_name | VARCHAR(20) | | 楼层名称展示,如「1层」「B1层」 |
|
||||||
|
| room_no | VARCHAR(30) | NOT NULL | 房号,如「01」「101」 |
|
||||||
|
| display_no | VARCHAR(50) | | 展示用完整房号,如「3-1-101」 |
|
||||||
|
| is_standard | BOOLEAN | NOT NULL DEFAULT FALSE | TRUE=已归一化为标准结构 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已拆除/不存在 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_room_units_building ON room_units(building_id) WHERE is_active = TRUE;
|
||||||
|
CREATE UNIQUE INDEX idx_room_units_unique ON room_units(building_id, floor, room_no) WHERE is_active = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.13 complex_photos — 楼盘照片
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||||
|
| category | VARCHAR(20) | NOT NULL | `complex`=楼盘图 / `layout`=户型图 / `vr`=VR全景 / `other`=其他 |
|
||||||
|
| file_key | TEXT | NOT NULL | R2/S3 路径 |
|
||||||
|
| thumbnail_key | TEXT | | 缩略图路径 |
|
||||||
|
| file_name | VARCHAR(255) | | |
|
||||||
|
| file_size | INTEGER | | bytes |
|
||||||
|
| width | INTEGER | | |
|
||||||
|
| height | INTEGER | | |
|
||||||
|
| is_cover | BOOLEAN | NOT NULL DEFAULT FALSE | 楼盘封面图 |
|
||||||
|
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_complex_photos_complex ON complex_photos(complex_id);
|
||||||
|
CREATE INDEX idx_complex_photos_category ON complex_photos(complex_id, category);
|
||||||
|
CREATE UNIQUE INDEX idx_complex_photos_cover ON complex_photos(complex_id) WHERE is_cover = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.14 complex_attachments — 楼盘附件
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||||
|
| file_key | TEXT | NOT NULL | |
|
||||||
|
| file_name | VARCHAR(255) | NOT NULL | |
|
||||||
|
| file_size | INTEGER | | |
|
||||||
|
| file_type | VARCHAR(50) | | MIME type |
|
||||||
|
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| created_by | UUID | FK→staff, SET NULL | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.15 complex_price_trends — 楼盘价格走势(月度)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||||
|
| record_month | DATE | NOT NULL | 月份(统一存为该月1日,如 2026-04-01) |
|
||||||
|
| avg_sale_price | NUMERIC(12,2) | | 月均售价(万元/套) |
|
||||||
|
| avg_unit_price | NUMERIC(10,2) | | 月均单价(元/m²) |
|
||||||
|
| transaction_count | INTEGER | | 成交套数 |
|
||||||
|
| listing_count | INTEGER | | 当月挂牌套数 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_complex_price_trend_month ON complex_price_trends(complex_id, record_month);
|
||||||
|
CREATE INDEX idx_complex_price_trend_complex ON complex_price_trends(complex_id, record_month DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、枚举常量
|
||||||
|
|
||||||
|
### complexes.building_type(建筑类型)
|
||||||
|
|
||||||
|
```
|
||||||
|
slab = 板楼
|
||||||
|
tower = 塔楼
|
||||||
|
slab_tower = 板塔结合
|
||||||
|
```
|
||||||
|
|
||||||
|
### complexes.water_type / electricity_type
|
||||||
|
|
||||||
|
```
|
||||||
|
civil = 民水/民电(住宅水电费率)
|
||||||
|
commercial = 商水/商电(商业水电费率,费用较高,影响买家决策)
|
||||||
|
```
|
||||||
|
|
||||||
|
### complex_schools.zone_type(学区类型)
|
||||||
|
|
||||||
|
```
|
||||||
|
guaranteed = 对口(直升)
|
||||||
|
reference = 参考(可能入读)
|
||||||
|
lottery = 摇号(通过摇号入学)
|
||||||
|
```
|
||||||
|
|
||||||
|
### buildings.is_standard / room_units.is_standard
|
||||||
|
|
||||||
|
```
|
||||||
|
TRUE = 已标准化(楼栋/房号已经运营核准,可用于精准房源定位)
|
||||||
|
FALSE = 非标(用户自输入,未核准,存在歧义风险)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、查询模式
|
||||||
|
|
||||||
|
### 5.1 楼盘名称联想搜索(录入房源时的自动补全)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 使用全文检索向量,支持中文分词近似匹配
|
||||||
|
SELECT id, name, address_summary, district_id
|
||||||
|
FROM complexes
|
||||||
|
WHERE search_vector @@ plainto_tsquery('simple', :keyword)
|
||||||
|
OR name ILIKE :keyword_prefix -- 前缀精确匹配优先
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND is_active = TRUE
|
||||||
|
ORDER BY
|
||||||
|
ts_rank(search_vector, plainto_tsquery('simple', :keyword)) DESC,
|
||||||
|
name
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 楼盘列表(含房源数量统计)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
c.id, c.name, c.address, c.latitude, c.longitude,
|
||||||
|
d.name AS district_name,
|
||||||
|
ba.name AS primary_business_area,
|
||||||
|
COUNT(DISTINCT b.id) AS building_count,
|
||||||
|
COUNT(DISTINCT p.id) FILTER (WHERE p.status IN ('for_sale','for_sale_rent')) AS sale_count,
|
||||||
|
COUNT(DISTINCT p.id) FILTER (WHERE p.status IN ('for_rent','for_sale_rent')) AS rent_count
|
||||||
|
FROM complexes c
|
||||||
|
LEFT JOIN districts d ON d.id = c.district_id
|
||||||
|
LEFT JOIN complex_business_areas cba ON cba.complex_id = c.id AND cba.is_primary = TRUE
|
||||||
|
LEFT JOIN business_areas ba ON ba.id = cba.business_area_id
|
||||||
|
LEFT JOIN buildings b ON b.complex_id = c.id AND b.is_active = TRUE
|
||||||
|
LEFT JOIN properties p ON p.complex_id = c.id AND p.deleted_at IS NULL
|
||||||
|
WHERE c.deleted_at IS NULL
|
||||||
|
AND c.district_id = ANY(:district_ids) -- 区域筛选
|
||||||
|
GROUP BY c.id, d.name, ba.name
|
||||||
|
ORDER BY c.name
|
||||||
|
LIMIT 20 OFFSET :offset;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 查询楼盘下的楼层-房号矩阵(结构管理)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 选中单元后,加载楼层×房号矩阵
|
||||||
|
SELECT
|
||||||
|
ru.floor,
|
||||||
|
ru.floor_name,
|
||||||
|
ru.room_no,
|
||||||
|
ru.display_no,
|
||||||
|
ru.is_standard,
|
||||||
|
p.id AS property_id, -- 如果该房号已有房源,关联显示
|
||||||
|
p.status AS property_status
|
||||||
|
FROM room_units ru
|
||||||
|
LEFT JOIN properties p ON p.building_id = ru.building_id
|
||||||
|
AND p.room_no = ru.room_no
|
||||||
|
AND p.floor = ru.floor
|
||||||
|
AND p.deleted_at IS NULL
|
||||||
|
WHERE ru.building_id = :building_id
|
||||||
|
AND ru.is_active = TRUE
|
||||||
|
ORDER BY ru.floor DESC, ru.room_no;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、触发器
|
||||||
|
|
||||||
|
### 6.1 楼盘全文检索向量(含别名)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
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.address_summary, '')), '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, address_summary, address
|
||||||
|
ON complexes
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_complex_search_vector();
|
||||||
|
|
||||||
|
-- 别名变更时同步更新楼盘 search_vector
|
||||||
|
CREATE OR REPLACE FUNCTION update_complex_search_on_alias()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE complexes
|
||||||
|
SET search_vector = (
|
||||||
|
setweight(to_tsvector('simple', COALESCE(name, '')), 'A') ||
|
||||||
|
setweight(to_tsvector('simple',
|
||||||
|
COALESCE((SELECT string_agg(alias, ' ') FROM complex_aliases WHERE complex_id = complexes.id), '')), 'B') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(address_summary, '')), 'C') ||
|
||||||
|
setweight(to_tsvector('simple', COALESCE(address, '')), 'D')
|
||||||
|
),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = COALESCE(NEW.complex_id, OLD.complex_id);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_complex_alias_search
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON complex_aliases
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_complex_search_on_alias();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、禁止操作
|
||||||
|
|
||||||
|
- ❌ **严禁直接修改 complexes.name**:楼盘名称变更必须走「楼盘合并」流程或「管理员申请」,通过 Application 层拦截任何直接 UPDATE `name` 字段的操作
|
||||||
|
- ❌ **严禁硬删除 complexes 记录**:有房源关联的楼盘不可删除(`RESTRICT` 外键),已有房源的楼盘软删除后房源仍可正常访问
|
||||||
|
- ❌ **严禁删除 complex_schools 关联而不清理房源学区标注**:必须在同一事务中清理对应 `property.school_ids` 数据
|
||||||
|
- ❌ **严禁在楼盘坐标为 NULL 时将其用于地图聚合**:坐标为空时不参与地图展示,过滤条件:`WHERE latitude IS NOT NULL`
|
||||||
|
- ❌ **严禁在 lock_info=TRUE 时绕过 Application 层直接修改楼盘信息字段**:锁定状态必须在服务层检查,不依赖数据库约束
|
||||||
|
- ❌ **严禁在没有 deleted_at IS NULL 过滤的情况下查询 complexes**:楼盘软删除过滤必须存在
|
||||||
341
Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md
Normal file
341
Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
# Fonrey — 组织人事数据模型(DATA_MODEL_ORG)
|
||||||
|
|
||||||
|
> **所属系统**: Fonrey 房产经纪管理系统
|
||||||
|
> **版本**: v1.0
|
||||||
|
> **日期**: 2026-04-24
|
||||||
|
> **关联模块**: `apps/org/` — 组织架构、员工档案、人事异动、账号体系
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、领域概览(Domain Overview)
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
- **OrgUnit(组织节点)**:公司组织树的节点,类型涵盖事业部 / 大区 / 区域 / 片区 / 门店 / 店组 / 职能。所有业务数据(房源、客源)最终归属到门店或店组级节点。
|
||||||
|
- **Staff(员工)**:系统的核心操作人员,与 Django `auth_user` 绑定登录账号,与 `org_units` 绑定岗位归属。员工的组织归属直接影响数据可见范围。
|
||||||
|
- **StaffTransferLog(人事异动记录)**:记录员工从入职到离职的全生命周期状态变化。每次异动(入职/调动/离职/复职)自动生成一条不可删除的日志。
|
||||||
|
- **StaffAccount(账号信息)**:员工的多平台登录账号体系,包括 Fonrey 主账号 / 58安居客 / 中国网络经纪人等。
|
||||||
|
|
||||||
|
### 关键业务规则
|
||||||
|
|
||||||
|
1. **组织层级约束**:店组级部门 **必须** 挂在门店下;经纪人/店管的所属部门 **只能** 是门店或店组。
|
||||||
|
2. **经纪人定义**:职务类别为「置业顾问」的员工即为经纪人,受业务规则特殊约束。
|
||||||
|
3. **人员异动强制日志**:入职、调动、离职、复职等操作均自动生成 `staff_transfer_logs` 记录,不可删除。
|
||||||
|
4. **账号与员工联动**:员工离职后,对应的 `auth_user.is_active` 设为 `False`,不可登录;复职后由管理员手动恢复。
|
||||||
|
5. **手机号敏感字段**:员工手机号 AES-256-GCM 加密存储,SHA-256 哈希用于唯一性校验,通讯录展示脱敏格式。
|
||||||
|
6. **数据归属继承**:员工调动时,名下房源/客源默认跟随员工到新部门;离职时可选择转移给指定账号。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、实体关系
|
||||||
|
|
||||||
|
```
|
||||||
|
OrgUnit (树形自引用,物化路径)
|
||||||
|
│
|
||||||
|
├── 1:N ── Staff (员工归属一个部门)
|
||||||
|
│ │
|
||||||
|
│ ├── 1:1 ── auth_user (Django 登录账号)
|
||||||
|
│ ├── 1:N ── StaffTransferLog (人事异动记录)
|
||||||
|
│ ├── 1:N ── StaffRewardPunish (奖惩记录)
|
||||||
|
│ ├── 1:N ── StaffAccount (第三方账号绑定)
|
||||||
|
│ └── 1:N ── StaffRemark (管理员备注)
|
||||||
|
│
|
||||||
|
└── 1:1 ── OrgUnit.parent_id (自引用)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Schema 定义
|
||||||
|
|
||||||
|
### 3.1 org_units — 组织节点表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 部门/组织名称 |
|
||||||
|
| type | VARCHAR(20) | NOT NULL, CHECK | 枚举:`company` / `division`(事业部) / `region`(大区) / `area`(区域) / `district`(片区) / `store`(门店) / `group`(店组) / `functional`(职能) |
|
||||||
|
| parent_id | UUID | FK→self, RESTRICT | 父节点,根节点为 NULL |
|
||||||
|
| path | TEXT | NOT NULL | 物化路径:`/root_id/.../self_id/`,用于子树查询 |
|
||||||
|
| depth | SMALLINT | NOT NULL DEFAULT 0 | 节点深度(根=0),最大支持 8 层 |
|
||||||
|
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 同级排序 |
|
||||||
|
| attribute | VARCHAR(10) | | 直营/加盟,枚举:`direct` / `franchise` |
|
||||||
|
| address_city | VARCHAR(50) | | 部门所在城市 |
|
||||||
|
| address_district | VARCHAR(50) | | 部门所在县区 |
|
||||||
|
| address_detail | VARCHAR(200) | | 详细地址 |
|
||||||
|
| latitude | NUMERIC(10,7) | | 坐标(部门定位针) |
|
||||||
|
| longitude | NUMERIC(10,7) | | 坐标 |
|
||||||
|
| manager_id | UUID | FK→staff.id, SET NULL | 部门负责人(循环依赖,Application 层维护) |
|
||||||
|
| established_at | DATE | | 成立时间 |
|
||||||
|
| phone | VARCHAR(30) | | 部门联系电话 |
|
||||||
|
| ext_start | INTEGER | | 分机号范围:起始 |
|
||||||
|
| ext_end | INTEGER | | 分机号范围:结束 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE = 已关闭部门,仍可在筛选中显示 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除 |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_org_units_parent ON org_units(parent_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_org_units_path_prefix ON org_units(path text_pattern_ops); -- 路径前缀查询
|
||||||
|
CREATE INDEX idx_org_units_type ON org_units(type) WHERE deleted_at IS NULL AND is_active = TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务注意**:
|
||||||
|
- 查询某部门及所有下级:`WHERE path LIKE '/root_id/{target_id}/%'`
|
||||||
|
- 店组(`group`)的 `parent_id` 必须指向一个 `store` 节点,新增前需校验
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 staff — 员工表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| org_unit_id | UUID | NOT NULL, FK→org_units | 当前所属组织节点(门店或店组) |
|
||||||
|
| user_id | INTEGER | UNIQUE, FK→auth_user | Django auth 登录账号 ID |
|
||||||
|
| name | VARCHAR(50) | NOT NULL | 真实姓名 |
|
||||||
|
| nickname | VARCHAR(50) | | 昵称(通讯录/显示名) |
|
||||||
|
| employee_no | VARCHAR(30) | UNIQUE | 员工工号,系统自动生成或手动录入 |
|
||||||
|
| role | VARCHAR(30) | NOT NULL, CHECK | 系统角色枚举:`agent`(经纪人) / `store_manager` / `area_manager` / `admin` / `operator` / `system` |
|
||||||
|
| job_title | VARCHAR(100) | | 职务名称,如「高级业务员」 |
|
||||||
|
| job_category | VARCHAR(50) | | 职务类别,如「置业顾问」(经纪人判定字段) |
|
||||||
|
| job_level | SMALLINT | | 职级(数字) |
|
||||||
|
| supervisor_id | UUID | FK→staff.id, SET NULL | 直属上级 |
|
||||||
|
| status | VARCHAR(20) | NOT NULL DEFAULT 'active' | `active`(在职) / `probation`(试用) / `resigned`(离职) / `frozen`(冻结) |
|
||||||
|
| phone_enc | BYTEA | | AES-256-GCM 加密手机号 |
|
||||||
|
| phone_hash | VARCHAR(64) | | SHA-256 哈希,用于唯一性索引 |
|
||||||
|
| phone_hide | BOOLEAN | NOT NULL DEFAULT FALSE | 通讯录是否隐藏手机号 |
|
||||||
|
| email | VARCHAR(255) | | 邮箱 |
|
||||||
|
| extension | VARCHAR(20) | | 分机号 |
|
||||||
|
| avatar_key | TEXT | | R2/S3 头像路径 |
|
||||||
|
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE 时账号不可登录(联动 auth_user.is_active) |
|
||||||
|
| is_system_admin | BOOLEAN | NOT NULL DEFAULT FALSE | 是否为系统管理员(影响权限上限) |
|
||||||
|
| first_joined_at | DATE | | 首次入职日期(计算工龄起点) |
|
||||||
|
| rejoined_at | DATE | | 最近复职日期 |
|
||||||
|
| resigned_at | DATE | | 最近离职日期 |
|
||||||
|
| joined_count | SMALLINT | NOT NULL DEFAULT 1 | 累计入职次数 |
|
||||||
|
| industry_exp_years | SMALLINT | | 行业经验(年) |
|
||||||
|
| mentor_id | UUID | FK→staff.id, SET NULL | 师傅(带教员工) |
|
||||||
|
| business_type | VARCHAR(50) | | 业务类型 |
|
||||||
|
| bank_name | VARCHAR(100) | | 银行名称 |
|
||||||
|
| bank_account | VARCHAR(50) | | 银行卡号(内部财务用) |
|
||||||
|
| partner_no | VARCHAR(50) | | 联号 |
|
||||||
|
| recruit_by_id | UUID | FK→staff.id, SET NULL | 招聘人 |
|
||||||
|
| recruit_source | VARCHAR(50) | | 招聘来源 |
|
||||||
|
| referrer_id | UUID | FK→staff.id, SET NULL | 转介人 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除(离职员工仍保留记录) |
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX idx_staff_employee_no ON staff(employee_no) WHERE deleted_at IS NULL;
|
||||||
|
CREATE UNIQUE INDEX idx_staff_phone_hash ON staff(phone_hash) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_staff_org_unit ON staff(org_unit_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_staff_supervisor ON staff(supervisor_id) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_staff_status ON staff(status) WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**业务注意**:
|
||||||
|
- `is_active = FALSE` 时对应 `auth_user.is_active` 同步设为 False,通过 Django signal 实现
|
||||||
|
- 离职员工(`status = 'resigned'`)不可硬删除,保留档案以便房源/客源历史关联查询
|
||||||
|
- 经纪人判定:`job_category = '置业顾问'`,部分权限逻辑基于此字段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 staff_personal_info — 员工个人信息扩展表
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| staff_id | UUID | UNIQUE, NOT NULL, FK→staff | 1:1 关系 |
|
||||||
|
| gender | VARCHAR(10) | | `male` / `female` / `unknown` |
|
||||||
|
| id_type | VARCHAR(20) | | 证件类型:`id_card`(身份证) / `passport` / `other` |
|
||||||
|
| id_number_enc | BYTEA | | 证件号码(AES 加密) |
|
||||||
|
| id_number_hash | VARCHAR(64) | | SHA-256 哈希(实名认证比对用) |
|
||||||
|
| id_verified | BOOLEAN | NOT NULL DEFAULT FALSE | 是否实名认证通过 |
|
||||||
|
| id_verified_at | TIMESTAMPTZ | | 认证时间 |
|
||||||
|
| birthdate | DATE | | 出生日期 |
|
||||||
|
| native_place | VARCHAR(100) | | 籍贯 |
|
||||||
|
| domicile_type | VARCHAR(20) | | 户籍性质 |
|
||||||
|
| marital_status | VARCHAR(20) | | 婚姻状况 |
|
||||||
|
| political_status | VARCHAR(20) | | 政治面貌 |
|
||||||
|
| has_children | BOOLEAN | | 有无子女 |
|
||||||
|
| education_level | VARCHAR(20) | | 最高学历 |
|
||||||
|
| ethnicity | VARCHAR(20) | | 民族 |
|
||||||
|
| domicile_address | VARCHAR(200) | | 户口所在地 |
|
||||||
|
| residence_address | VARCHAR(200) | | 居住地址 |
|
||||||
|
| work_start_date | DATE | | 参加工作时间 |
|
||||||
|
| emergency_contact | VARCHAR(50) | | 紧急联系人 |
|
||||||
|
| emergency_phone_enc | BYTEA | | 紧急联系人电话(加密) |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| updated_by | UUID | FK→staff.id, SET NULL | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 staff_transfer_logs — 人事异动记录
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| staff_id | UUID | NOT NULL, FK→staff, RESTRICT | 被操作员工 |
|
||||||
|
| transfer_type | VARCHAR(30) | NOT NULL, CHECK | 枚举见下方 |
|
||||||
|
| old_value | JSONB | | 变动前的值快照,格式:`{"field": "org_unit_id", "value": "...", "label": "门店A"}` |
|
||||||
|
| new_value | JSONB | | 变动后的值快照 |
|
||||||
|
| transfer_date | DATE | NOT NULL | 异动生效日期(可以是过去日期) |
|
||||||
|
| remarks | VARCHAR(50) | | 备注(最多50字) |
|
||||||
|
| operator_id | UUID | NOT NULL, FK→staff, RESTRICT | 操作人 |
|
||||||
|
| operated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 系统操作时间 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| ⚠️ 无 deleted_at | — | — | 异动记录**不可删除** |
|
||||||
|
|
||||||
|
**transfer_type 枚举**:
|
||||||
|
```
|
||||||
|
onboard = 入职
|
||||||
|
transfer = 调动(含平调/晋升/降职)
|
||||||
|
resign = 离职
|
||||||
|
rejoin = 复职
|
||||||
|
supervisor_change = 上级变动
|
||||||
|
role_change = 角色变更
|
||||||
|
freeze = 账号冻结
|
||||||
|
unfreeze = 账号恢复
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键索引**:
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_transfer_logs_staff ON staff_transfer_logs(staff_id, transfer_date DESC);
|
||||||
|
CREATE INDEX idx_transfer_logs_type ON staff_transfer_logs(transfer_type, operated_at DESC);
|
||||||
|
CREATE INDEX idx_transfer_logs_operator ON staff_transfer_logs(operator_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 staff_reward_punish — 奖惩记录
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| staff_id | UUID | NOT NULL, FK→staff | |
|
||||||
|
| rp_date | DATE | NOT NULL | 奖惩日期 |
|
||||||
|
| category | VARCHAR(50) | NOT NULL | 奖惩类别(枚举由 lookup 表维护) |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 奖惩名称(与类别联动) |
|
||||||
|
| remarks | TEXT | | 备注 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| created_by | UUID | FK→staff.id, SET NULL | |
|
||||||
|
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
| deleted_at | TIMESTAMPTZ | | 软删除 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 staff_work_experiences / staff_educations / staff_trainings / staff_family_members
|
||||||
|
|
||||||
|
这四张表结构类似,均为 1:N 附属于 `staff`,存储员工档案中「工作经历」「教育经历」「培训经历」「家庭主要成员」信息。详见下方汇总:
|
||||||
|
|
||||||
|
| 表名 | 关键字段 |
|
||||||
|
|------|---------|
|
||||||
|
| `staff_work_experiences` | staff_id, company, job_title, start_date, end_date, reason, reference_name, reference_phone |
|
||||||
|
| `staff_educations` | staff_id, stage, school, major, start_date, end_date, enrollment_status, degree |
|
||||||
|
| `staff_trainings` | staff_id, training_name, training_date, certificate |
|
||||||
|
| `staff_family_members` | staff_id, relation(称谓), name, birthdate, occupation, work_unit, phone_enc |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 staff_accounts — 员工第三方账号绑定
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 业务说明 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| id | UUID | PK | |
|
||||||
|
| staff_id | UUID | NOT NULL, FK→staff | |
|
||||||
|
| platform | VARCHAR(30) | NOT NULL, CHECK | `fonrey`(主账号) / `58anjuke` / `cnreic`(中国网络经纪人) / `wechat_mp`(微信公众号) |
|
||||||
|
| account_no | VARCHAR(100) | | 账号/手机号 |
|
||||||
|
| is_real_name_match | BOOLEAN | | 实名信息一致性(中国网络经纪人专用) |
|
||||||
|
| is_bound | BOOLEAN | NOT NULL DEFAULT FALSE | 是否已绑定 |
|
||||||
|
| bound_at | TIMESTAMPTZ | | 绑定时间 |
|
||||||
|
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、枚举常量
|
||||||
|
|
||||||
|
### Staff.role(系统角色)
|
||||||
|
|
||||||
|
| 值 | 含义 | 数据可见范围默认 |
|
||||||
|
|----|------|----------------|
|
||||||
|
| `agent` | 一线经纪人 | 本人/本组 |
|
||||||
|
| `store_manager` | 店长 | 本门店 |
|
||||||
|
| `area_manager` | 区域经理 | 本区域 |
|
||||||
|
| `admin` | 系统管理员 | 全公司 |
|
||||||
|
| `operator` | 运营/行政 | 全公司(只读为主) |
|
||||||
|
| `system` | 系统账号(定时任务用) | — |
|
||||||
|
|
||||||
|
### Staff.status(员工状态)
|
||||||
|
|
||||||
|
```
|
||||||
|
active = 正式在职
|
||||||
|
probation = 试用期
|
||||||
|
resigned = 已离职(不可删除,保留档案)
|
||||||
|
frozen = 账号冻结(在职但无法登录)
|
||||||
|
```
|
||||||
|
|
||||||
|
### OrgUnit.type(组织类型)
|
||||||
|
|
||||||
|
```
|
||||||
|
company = 公司根节点(每个租户唯一)
|
||||||
|
division = 事业部
|
||||||
|
region = 大区
|
||||||
|
area = 区域
|
||||||
|
district = 片区
|
||||||
|
store = 门店(经纪人最小归属单位)
|
||||||
|
group = 店组(门店下的业务小组)
|
||||||
|
functional = 职能部门(行政/财务等)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、查询模式
|
||||||
|
|
||||||
|
### 5.1 查询某部门及所有下级的在职员工
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 利用物化路径高效查询子树
|
||||||
|
SELECT s.*
|
||||||
|
FROM staff s
|
||||||
|
JOIN org_units ou ON s.org_unit_id = ou.id
|
||||||
|
WHERE ou.path LIKE '/root_id/{target_org_unit_id}/%'
|
||||||
|
OR ou.id = '{target_org_unit_id}'
|
||||||
|
AND s.deleted_at IS NULL
|
||||||
|
AND s.status != 'resigned';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 查询员工完整异动历史
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT stl.*,
|
||||||
|
s.name as operator_name,
|
||||||
|
ou.name as operator_org
|
||||||
|
FROM staff_transfer_logs stl
|
||||||
|
JOIN staff s ON stl.operator_id = s.id
|
||||||
|
JOIN org_units ou ON s.org_unit_id = ou.id
|
||||||
|
WHERE stl.staff_id = :staff_id
|
||||||
|
ORDER BY stl.transfer_date DESC, stl.operated_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 获取员工的直接上下级链
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 直属上级
|
||||||
|
SELECT supervisor.* FROM staff
|
||||||
|
JOIN staff supervisor ON staff.supervisor_id = supervisor.id
|
||||||
|
WHERE staff.id = :staff_id AND supervisor.deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、禁止操作
|
||||||
|
|
||||||
|
- ❌ **严禁硬删除 staff 记录**:离职员工需通过 `deleted_at + status = 'resigned'` 软删除,历史房源/跟进日志依赖 `staff.id` 外键
|
||||||
|
- ❌ **严禁删除 staff_transfer_logs**:异动记录为不可变审计日志
|
||||||
|
- ❌ **严禁直接修改 staff.user_id**:账号绑定关系变更需走专门的账号管理流程
|
||||||
|
- ❌ **严禁绕过组织层级约束**:店组不在门店下的数据操作需在 Application 层校验并拒绝
|
||||||
|
- ❌ **严禁明文存储员工手机号和证件号**:必须走 `EncryptedPhoneField` / `EncryptedIDField`
|
||||||
574
Project/fonrey/DATA_MODEL/diagram/fonrey-er.svg
Normal file
574
Project/fonrey/DATA_MODEL/diagram/fonrey-er.svg
Normal file
@@ -0,0 +1,574 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2560 1980">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
||||||
|
text { font-family: 'JetBrains Mono', 'Noto Sans SC', 'PingFang SC', 'SF Mono', monospace; }
|
||||||
|
</style>
|
||||||
|
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" stroke-width="0.5"/>
|
||||||
|
</pattern>
|
||||||
|
<!-- Arrow markers per color -->
|
||||||
|
<marker id="arrow-cyan" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||||
|
<polygon points="0 0, 10 3.5, 0 7" fill="#22d3ee"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="arrow-emerald" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||||
|
<polygon points="0 0, 10 3.5, 0 7" fill="#34d399"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="arrow-violet" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||||
|
<polygon points="0 0, 10 3.5, 0 7" fill="#a78bfa"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="arrow-amber" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||||
|
<polygon points="0 0, 10 3.5, 0 7" fill="#fbbf24"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="arrow-slate" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||||
|
<polygon points="0 0, 10 3.5, 0 7" fill="#94a3b8"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="arrow-orange" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||||
|
<polygon points="0 0, 10 3.5, 0 7" fill="#fb923c"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="2560" height="1980" fill="#0f172a"/>
|
||||||
|
<rect width="2560" height="1980" fill="url(#grid)"/>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
<!-- MODULE BOUNDARIES -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- Org Module boundary (cyan) -->
|
||||||
|
<rect x="30" y="60" width="310" height="680" rx="12" fill="none" stroke="#22d3ee" stroke-width="1" stroke-dasharray="8,4" opacity="0.6"/>
|
||||||
|
<text x="44" y="80" fill="#22d3ee" font-size="10" font-weight="600">ORG / HR</text>
|
||||||
|
|
||||||
|
<!-- Region+Complex Module boundary (emerald) -->
|
||||||
|
<rect x="360" y="60" width="680" height="1200" rx="12" fill="none" stroke="#34d399" stroke-width="1" stroke-dasharray="8,4" opacity="0.6"/>
|
||||||
|
<text x="374" y="80" fill="#34d399" font-size="10" font-weight="600">REGION & COMPLEX</text>
|
||||||
|
|
||||||
|
<!-- Property Module boundary (violet) -->
|
||||||
|
<rect x="1060" y="60" width="720" height="1560" rx="12" fill="none" stroke="#a78bfa" stroke-width="1" stroke-dasharray="8,4" opacity="0.6"/>
|
||||||
|
<text x="1074" y="80" fill="#a78bfa" font-size="10" font-weight="600">PROPERTY</text>
|
||||||
|
|
||||||
|
<!-- Client Module boundary (amber) -->
|
||||||
|
<rect x="1800" y="60" width="730" height="1200" rx="12" fill="none" stroke="#fbbf24" stroke-width="1" stroke-dasharray="8,4" opacity="0.6"/>
|
||||||
|
<text x="1814" y="80" fill="#fbbf24" font-size="10" font-weight="600">CLIENT</text>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
<!-- CONNECTION LINES (drawn before boxes) -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- OrgUnit → Staff (1:N) -->
|
||||||
|
<line x1="185" y1="220" x2="185" y2="320" stroke="#22d3ee" stroke-width="1.2" marker-end="url(#arrow-cyan)"/>
|
||||||
|
<text x="193" y="275" fill="#22d3ee" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Staff → Property (1:N, via created_by) -->
|
||||||
|
<line x1="335" y1="370" x2="1060" y2="370" stroke="#22d3ee" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-cyan)"/>
|
||||||
|
<text x="690" y="362" fill="#22d3ee" font-size="8">created_by</text>
|
||||||
|
|
||||||
|
<!-- Staff → Client (1:N, via agent) -->
|
||||||
|
<line x1="335" y1="410" x2="1800" y2="410" stroke="#22d3ee" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-cyan)"/>
|
||||||
|
<text x="1060" y="402" fill="#22d3ee" font-size="8">agent_id</text>
|
||||||
|
|
||||||
|
<!-- District → BusinessArea (1:N) -->
|
||||||
|
<line x1="560" y1="225" x2="560" y2="320" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||||
|
<text x="568" y="277" fill="#34d399" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- District → School (1:N) -->
|
||||||
|
<line x1="700" y1="175" x2="870" y2="175" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||||
|
<text x="775" y="167" fill="#34d399" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- BusinessArea ↔ Complex (N:M via complex_business_areas) -->
|
||||||
|
<line x1="560" y1="420" x2="560" y2="500" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||||
|
<text x="568" y="464" fill="#34d399" font-size="8">N:M</text>
|
||||||
|
|
||||||
|
<!-- Complex → Complex_schools join label -->
|
||||||
|
<line x1="700" y1="570" x2="870" y2="400" stroke="#34d399" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-emerald)"/>
|
||||||
|
<text x="780" y="495" fill="#34d399" font-size="8">N:M</text>
|
||||||
|
|
||||||
|
<!-- Complex → Building (1:N) -->
|
||||||
|
<line x1="560" y1="700" x2="560" y2="790" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||||
|
<text x="568" y="749" fill="#34d399" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Building → RoomUnit (1:N) -->
|
||||||
|
<line x1="560" y1="980" x2="560" y2="1060" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||||
|
<text x="568" y="1024" fill="#34d399" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Complex → Property (1:N) -->
|
||||||
|
<line x1="720" y1="600" x2="1060" y2="300" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||||
|
<text x="885" y="445" fill="#a78bfa" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Property → PropertyContact (1:N) -->
|
||||||
|
<line x1="1300" y1="390" x2="1300" y2="490" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||||
|
<text x="1308" y="444" fill="#a78bfa" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Property → FollowLog (1:N) -->
|
||||||
|
<line x1="1420" y1="300" x2="1570" y2="300" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||||
|
<text x="1482" y="292" fill="#a78bfa" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Property → PropertyPhoto (1:N) -->
|
||||||
|
<line x1="1300" y1="670" x2="1300" y2="760" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||||
|
<text x="1308" y="718" fill="#a78bfa" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Property → KeyManagement (1:N) -->
|
||||||
|
<line x1="1420" y1="550" x2="1570" y2="550" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||||
|
<text x="1482" y="542" fill="#a78bfa" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Property → Commission (1:N) -->
|
||||||
|
<line x1="1420" y1="650" x2="1570" y2="750" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||||
|
<text x="1490" y="695" fill="#a78bfa" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Property → Inspection (1:N) -->
|
||||||
|
<line x1="1300" y1="940" x2="1300" y2="1020" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||||
|
<text x="1308" y="984" fill="#a78bfa" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Property → Marketing (1:1) -->
|
||||||
|
<line x1="1420" y1="870" x2="1570" y2="960" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||||
|
<text x="1490" y="910" fill="#a78bfa" font-size="8">1:1</text>
|
||||||
|
|
||||||
|
<!-- Property → ListingHistory (1:N) -->
|
||||||
|
<line x1="1180" y1="390" x2="1080" y2="490" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||||
|
<text x="1100" y="435" fill="#a78bfa" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Client → ClientRequirement (1:N) -->
|
||||||
|
<line x1="2050" y1="220" x2="2050" y2="310" stroke="#fbbf24" stroke-width="1.2" marker-end="url(#arrow-amber)"/>
|
||||||
|
<text x="2058" y="269" fill="#fbbf24" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Client → ClientFollowLog (1:N) -->
|
||||||
|
<line x1="1930" y1="220" x2="1830" y2="310" stroke="#fbbf24" stroke-width="1.2" marker-end="url(#arrow-amber)"/>
|
||||||
|
<text x="1850" y="260" fill="#fbbf24" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Client → Viewing (1:N) -->
|
||||||
|
<line x1="2170" y1="220" x2="2270" y2="310" stroke="#fbbf24" stroke-width="1.2" marker-end="url(#arrow-amber)"/>
|
||||||
|
<text x="2210" y="260" fill="#fbbf24" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Client → Match (1:N) -->
|
||||||
|
<line x1="2050" y1="490" x2="2050" y2="580" stroke="#fbbf24" stroke-width="1.2" marker-end="url(#arrow-amber)"/>
|
||||||
|
<text x="2058" y="538" fill="#fbbf24" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Property → Viewing (1:N) -->
|
||||||
|
<line x1="1780" y1="340" x2="2270" y2="340" stroke="#fb923c" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-orange)"/>
|
||||||
|
<text x="2020" y="332" fill="#fb923c" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- Property → Match (1:N) -->
|
||||||
|
<line x1="1780" y1="620" x2="1950" y2="620" stroke="#fb923c" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-orange)"/>
|
||||||
|
<text x="1845" y="612" fill="#fb923c" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- MetroStation → Complex (N:M) -->
|
||||||
|
<line x1="620" y1="1280" x2="620" y2="1200" stroke="#34d399" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-emerald)"/>
|
||||||
|
<text x="628" y="1244" fill="#34d399" font-size="8">N:M</text>
|
||||||
|
|
||||||
|
<!-- MetroLine → MetroStation (1:N) -->
|
||||||
|
<line x1="430" y1="1280" x2="500" y2="1280" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||||
|
<text x="452" y="1272" fill="#34d399" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- ComplexPriceTrend → Complex -->
|
||||||
|
<line x1="870" y1="900" x2="720" y2="660" stroke="#34d399" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-emerald)"/>
|
||||||
|
<text x="790" y="800" fill="#34d399" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
<!-- ORG MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- OrgUnit -->
|
||||||
|
<rect x="80" y="100" width="210" height="120" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="80" y="100" width="210" height="120" rx="6" fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||||
|
<line x1="80" y1="128" x2="290" y2="128" stroke="#22d3ee" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="185" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">org_units</text>
|
||||||
|
<text x="90" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="90" y="158" fill="#94a3b8" font-size="8">parent_id: uuid (FK→self)</text>
|
||||||
|
<text x="90" y="171" fill="#94a3b8" font-size="8">type: varchar(20)</text>
|
||||||
|
<text x="90" y="184" fill="#94a3b8" font-size="8">name, path, depth</text>
|
||||||
|
<text x="90" y="197" fill="#94a3b8" font-size="8">is_active: bool</text>
|
||||||
|
|
||||||
|
<!-- Staff -->
|
||||||
|
<rect x="80" y="320" width="210" height="150" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="80" y="320" width="210" height="150" rx="6" fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||||
|
<line x1="80" y1="348" x2="290" y2="348" stroke="#22d3ee" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="185" y="340" fill="white" font-size="11" font-weight="700" text-anchor="middle">staff</text>
|
||||||
|
<text x="90" y="365" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="90" y="378" fill="#94a3b8" font-size="8">FK org_unit_id</text>
|
||||||
|
<text x="90" y="391" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||||
|
<text x="90" y="404" fill="#94a3b8" font-size="8">phone_enc: text (AES)</text>
|
||||||
|
<text x="90" y="417" fill="#94a3b8" font-size="8">phone_hash: varchar(64)</text>
|
||||||
|
<text x="90" y="430" fill="#94a3b8" font-size="8">user_id: uuid (FK→auth)</text>
|
||||||
|
<text x="90" y="443" fill="#94a3b8" font-size="8">is_active, deleted_at</text>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
<!-- REGION + COMPLEX MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- District -->
|
||||||
|
<rect x="440" y="100" width="220" height="125" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="440" y="100" width="220" height="125" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||||
|
<line x1="440" y1="128" x2="660" y2="128" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="550" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">districts</text>
|
||||||
|
<text x="450" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="450" y="158" fill="#94a3b8" font-size="8">city: varchar(50)</text>
|
||||||
|
<text x="450" y="171" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||||
|
<text x="450" y="184" fill="#94a3b8" font-size="8">short_name: varchar(20)</text>
|
||||||
|
<text x="450" y="197" fill="#94a3b8" font-size="8">sort_order, is_active</text>
|
||||||
|
|
||||||
|
<!-- BusinessArea -->
|
||||||
|
<rect x="440" y="320" width="240" height="135" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="440" y="320" width="240" height="135" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||||
|
<line x1="440" y1="348" x2="680" y2="348" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="560" y="340" fill="white" font-size="11" font-weight="700" text-anchor="middle">business_areas</text>
|
||||||
|
<text x="450" y="365" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="450" y="378" fill="#94a3b8" font-size="8">FK district_id</text>
|
||||||
|
<text x="450" y="391" fill="#94a3b8" font-size="8">name: varchar(100)</text>
|
||||||
|
<text x="450" y="404" fill="#94a3b8" font-size="8">latitude, longitude</text>
|
||||||
|
<text x="450" y="417" fill="#94a3b8" font-size="8">sort_order, is_active</text>
|
||||||
|
|
||||||
|
<!-- School -->
|
||||||
|
<rect x="700" y="100" width="220" height="135" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="700" y="100" width="220" height="135" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||||
|
<line x1="700" y1="128" x2="920" y2="128" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="810" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">schools</text>
|
||||||
|
<text x="710" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="710" y="158" fill="#94a3b8" font-size="8">FK district_id</text>
|
||||||
|
<text x="710" y="171" fill="#94a3b8" font-size="8">name: varchar(100)</text>
|
||||||
|
<text x="710" y="184" fill="#94a3b8" font-size="8">type: primary/middle/high</text>
|
||||||
|
<text x="710" y="197" fill="#94a3b8" font-size="8">nature: public/private</text>
|
||||||
|
<text x="710" y="210" fill="#94a3b8" font-size="8">level: normal/key/top</text>
|
||||||
|
|
||||||
|
<!-- Complex -->
|
||||||
|
<rect x="440" y="500" width="300" height="200" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="440" y="500" width="300" height="200" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||||
|
<line x1="440" y1="528" x2="740" y2="528" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="590" y="520" fill="white" font-size="11" font-weight="700" text-anchor="middle">complexes</text>
|
||||||
|
<text x="450" y="545" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="450" y="558" fill="#94a3b8" font-size="8">name: varchar(200) [不可直接修改]</text>
|
||||||
|
<text x="450" y="571" fill="#94a3b8" font-size="8">FK district_id</text>
|
||||||
|
<text x="450" y="584" fill="#94a3b8" font-size="8">address, address_summary</text>
|
||||||
|
<text x="450" y="597" fill="#94a3b8" font-size="8">latitude, longitude</text>
|
||||||
|
<text x="450" y="610" fill="#94a3b8" font-size="8">lock_building/room/info: bool</text>
|
||||||
|
<text x="450" y="623" fill="#94a3b8" font-size="8">property_usage_types: varchar[]</text>
|
||||||
|
<text x="450" y="636" fill="#94a3b8" font-size="8">search_vector: tsvector</text>
|
||||||
|
<text x="450" y="649" fill="#94a3b8" font-size="8">developer, property_company</text>
|
||||||
|
<text x="450" y="662" fill="#94a3b8" font-size="8">deleted_at, created_by</text>
|
||||||
|
<text x="450" y="675" fill="#94a3b8" font-size="8">...</text>
|
||||||
|
|
||||||
|
<!-- complex_business_areas join table (small) -->
|
||||||
|
<rect x="440" y="465" width="220" height="30" rx="4" fill="#0f172a"/>
|
||||||
|
<rect x="440" y="465" width="220" height="30" rx="4" fill="rgba(6,78,59,0.2)" stroke="#34d399" stroke-width="1" stroke-dasharray="3,2"/>
|
||||||
|
<text x="550" y="484" fill="#34d399" font-size="8" text-anchor="middle">complex_business_areas (N:M) · is_primary</text>
|
||||||
|
|
||||||
|
<!-- complex_schools join table (small) -->
|
||||||
|
<rect x="700" y="310" width="210" height="30" rx="4" fill="#0f172a"/>
|
||||||
|
<rect x="700" y="310" width="210" height="30" rx="4" fill="rgba(6,78,59,0.2)" stroke="#34d399" stroke-width="1" stroke-dasharray="3,2"/>
|
||||||
|
<text x="805" y="329" fill="#34d399" font-size="8" text-anchor="middle">complex_schools · zone_type</text>
|
||||||
|
|
||||||
|
<!-- Building -->
|
||||||
|
<rect x="440" y="790" width="300" height="185" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="440" y="790" width="300" height="185" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||||
|
<line x1="440" y1="818" x2="740" y2="818" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="590" y="810" fill="white" font-size="11" font-weight="700" text-anchor="middle">buildings</text>
|
||||||
|
<text x="450" y="835" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="450" y="848" fill="#94a3b8" font-size="8">FK complex_id</text>
|
||||||
|
<text x="450" y="861" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||||
|
<text x="450" y="874" fill="#94a3b8" font-size="8">is_standard: bool</text>
|
||||||
|
<text x="450" y="887" fill="#94a3b8" font-size="8">total_floors: smallint</text>
|
||||||
|
<text x="450" y="900" fill="#94a3b8" font-size="8">has_elevator: bool</text>
|
||||||
|
<text x="450" y="913" fill="#94a3b8" font-size="8">built_year: smallint</text>
|
||||||
|
<text x="450" y="926" fill="#94a3b8" font-size="8">property_usage_type</text>
|
||||||
|
<text x="450" y="939" fill="#94a3b8" font-size="8">is_active, created_at</text>
|
||||||
|
<text x="450" y="952" fill="#94a3b8" font-size="8">FK school_id</text>
|
||||||
|
|
||||||
|
<!-- RoomUnit -->
|
||||||
|
<rect x="440" y="1060" width="300" height="155" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="440" y="1060" width="300" height="155" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||||
|
<line x1="440" y1="1088" x2="740" y2="1088" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="590" y="1080" fill="white" font-size="11" font-weight="700" text-anchor="middle">room_units</text>
|
||||||
|
<text x="450" y="1105" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="450" y="1118" fill="#94a3b8" font-size="8">FK building_id</text>
|
||||||
|
<text x="450" y="1131" fill="#94a3b8" font-size="8">floor: smallint</text>
|
||||||
|
<text x="450" y="1144" fill="#94a3b8" font-size="8">floor_name: varchar(20)</text>
|
||||||
|
<text x="450" y="1157" fill="#94a3b8" font-size="8">room_no: varchar(30)</text>
|
||||||
|
<text x="450" y="1170" fill="#94a3b8" font-size="8">display_no: varchar(50)</text>
|
||||||
|
<text x="450" y="1183" fill="#94a3b8" font-size="8">is_standard: bool</text>
|
||||||
|
<text x="450" y="1196" fill="#94a3b8" font-size="8">is_active</text>
|
||||||
|
|
||||||
|
<!-- ComplexPriceTrend -->
|
||||||
|
<rect x="770" y="800" width="250" height="140" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="770" y="800" width="250" height="140" rx="6" fill="rgba(6,78,59,0.3)" stroke="#34d399" stroke-width="1.5"/>
|
||||||
|
<line x1="770" y1="828" x2="1020" y2="828" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="895" y="820" fill="white" font-size="11" font-weight="700" text-anchor="middle">complex_price_trends</text>
|
||||||
|
<text x="780" y="845" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="780" y="858" fill="#94a3b8" font-size="8">FK complex_id</text>
|
||||||
|
<text x="780" y="871" fill="#94a3b8" font-size="8">record_month: date</text>
|
||||||
|
<text x="780" y="884" fill="#94a3b8" font-size="8">avg_unit_price: numeric(10,2)</text>
|
||||||
|
<text x="780" y="897" fill="#94a3b8" font-size="8">avg_sale_price: numeric(12,2)</text>
|
||||||
|
<text x="780" y="910" fill="#94a3b8" font-size="8">transaction_count: int</text>
|
||||||
|
<text x="780" y="923" fill="#94a3b8" font-size="8">listing_count: int</text>
|
||||||
|
|
||||||
|
<!-- MetroLine -->
|
||||||
|
<rect x="370" y="1270" width="200" height="105" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="370" y="1270" width="200" height="105" rx="6" fill="rgba(6,78,59,0.3)" stroke="#34d399" stroke-width="1.5"/>
|
||||||
|
<line x1="370" y1="1298" x2="570" y2="1298" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="470" y="1290" fill="white" font-size="11" font-weight="700" text-anchor="middle">metro_lines</text>
|
||||||
|
<text x="380" y="1315" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="380" y="1328" fill="#94a3b8" font-size="8">city: varchar(50)</text>
|
||||||
|
<text x="380" y="1341" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||||
|
<text x="380" y="1354" fill="#94a3b8" font-size="8">color: varchar(7) [HEX]</text>
|
||||||
|
|
||||||
|
<!-- MetroStation -->
|
||||||
|
<rect x="580" y="1270" width="240" height="120" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="580" y="1270" width="240" height="120" rx="6" fill="rgba(6,78,59,0.3)" stroke="#34d399" stroke-width="1.5"/>
|
||||||
|
<line x1="580" y1="1298" x2="820" y2="1298" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="700" y="1290" fill="white" font-size="11" font-weight="700" text-anchor="middle">metro_stations</text>
|
||||||
|
<text x="590" y="1315" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="590" y="1328" fill="#94a3b8" font-size="8">FK metro_line_id</text>
|
||||||
|
<text x="590" y="1341" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||||
|
<text x="590" y="1354" fill="#94a3b8" font-size="8">latitude, longitude</text>
|
||||||
|
<text x="590" y="1367" fill="#94a3b8" font-size="8">sort_order</text>
|
||||||
|
|
||||||
|
<!-- complex_metro_stations join (small) -->
|
||||||
|
<rect x="580" y="1200" width="240" height="28" rx="4" fill="#0f172a"/>
|
||||||
|
<rect x="580" y="1200" width="240" height="28" rx="4" fill="rgba(6,78,59,0.2)" stroke="#34d399" stroke-width="1" stroke-dasharray="3,2"/>
|
||||||
|
<text x="700" y="1218" fill="#34d399" font-size="8" text-anchor="middle">complex_metro_stations · distance_meters</text>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
<!-- PROPERTY MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- Property -->
|
||||||
|
<rect x="1080" y="100" width="340" height="290" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1080" y="100" width="340" height="290" rx="6" fill="rgba(76,29,149,0.4)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||||
|
<line x1="1080" y1="128" x2="1420" y2="128" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="1250" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">properties</text>
|
||||||
|
<text x="1090" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1090" y="158" fill="#94a3b8" font-size="8">FK complex_id FK building_id</text>
|
||||||
|
<text x="1090" y="171" fill="#94a3b8" font-size="8">FK room_unit_id FK agent_id</text>
|
||||||
|
<text x="1090" y="184" fill="#94a3b8" font-size="8">listing_type: sale/rent/both</text>
|
||||||
|
<text x="1090" y="197" fill="#94a3b8" font-size="8">status: varchar(20)</text>
|
||||||
|
<text x="1090" y="210" fill="#94a3b8" font-size="8">sale_price: numeric(12,2)</text>
|
||||||
|
<text x="1090" y="223" fill="#94a3b8" font-size="8">rent_price: numeric(10,2)</text>
|
||||||
|
<text x="1090" y="236" fill="#94a3b8" font-size="8">floor, total_floors</text>
|
||||||
|
<text x="1090" y="249" fill="#94a3b8" font-size="8">area: numeric(8,2) [m²]</text>
|
||||||
|
<text x="1090" y="262" fill="#94a3b8" font-size="8">bedroom, living, bathroom</text>
|
||||||
|
<text x="1090" y="275" fill="#94a3b8" font-size="8">orientation, decoration</text>
|
||||||
|
<text x="1090" y="288" fill="#94a3b8" font-size="8">search_vector: tsvector</text>
|
||||||
|
<text x="1090" y="301" fill="#94a3b8" font-size="8">is_exclusive: bool</text>
|
||||||
|
<text x="1090" y="314" fill="#94a3b8" font-size="8">completeness_score: int</text>
|
||||||
|
<text x="1090" y="327" fill="#94a3b8" font-size="8">deleted_at, created_by</text>
|
||||||
|
<text x="1090" y="340" fill="#94a3b8" font-size="8">...</text>
|
||||||
|
<text x="1090" y="353" fill="#94a3b8" font-size="7">[89,000+ rows · partitioned by status]</text>
|
||||||
|
<text x="1090" y="370" fill="#94a3b8" font-size="7">UNIQUE (complex_id, building_id, floor, room_no)</text>
|
||||||
|
|
||||||
|
<!-- PropertyContact -->
|
||||||
|
<rect x="1080" y="490" width="280" height="155" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1080" y="490" width="280" height="155" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||||
|
<line x1="1080" y1="518" x2="1360" y2="518" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="1220" y="510" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_contacts</text>
|
||||||
|
<text x="1090" y="535" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1090" y="548" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||||
|
<text x="1090" y="561" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||||
|
<text x="1090" y="574" fill="#94a3b8" font-size="8">phone_enc: text (AES)</text>
|
||||||
|
<text x="1090" y="587" fill="#94a3b8" font-size="8">phone_hash: varchar(64)</text>
|
||||||
|
<text x="1090" y="600" fill="#94a3b8" font-size="8">role: owner/agent/tenant</text>
|
||||||
|
<text x="1090" y="613" fill="#94a3b8" font-size="8">is_primary: bool</text>
|
||||||
|
|
||||||
|
<!-- PropertyPhoto -->
|
||||||
|
<rect x="1080" y="760" width="280" height="155" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1080" y="760" width="280" height="155" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||||
|
<line x1="1080" y1="788" x2="1360" y2="788" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="1220" y="780" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_photos</text>
|
||||||
|
<text x="1090" y="805" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1090" y="818" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||||
|
<text x="1090" y="831" fill="#94a3b8" font-size="8">category: listing/vr/layout</text>
|
||||||
|
<text x="1090" y="844" fill="#94a3b8" font-size="8">file_key: text (R2/S3)</text>
|
||||||
|
<text x="1090" y="857" fill="#94a3b8" font-size="8">is_cover: bool</text>
|
||||||
|
<text x="1090" y="870" fill="#94a3b8" font-size="8">sort_order: smallint</text>
|
||||||
|
<text x="1090" y="883" fill="#94a3b8" font-size="8">width, height, file_size</text>
|
||||||
|
|
||||||
|
<!-- Inspection -->
|
||||||
|
<rect x="1080" y="1020" width="280" height="135" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1080" y="1020" width="280" height="135" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||||
|
<line x1="1080" y1="1048" x2="1360" y2="1048" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="1220" y="1040" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_inspections</text>
|
||||||
|
<text x="1090" y="1065" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1090" y="1078" fill="#94a3b8" font-size="8">FK property_id FK staff_id</text>
|
||||||
|
<text x="1090" y="1091" fill="#94a3b8" font-size="8">inspected_at: timestamptz</text>
|
||||||
|
<text x="1090" y="1104" fill="#94a3b8" font-size="8">status: pending/done</text>
|
||||||
|
<text x="1090" y="1117" fill="#94a3b8" font-size="8">notes: text</text>
|
||||||
|
<text x="1090" y="1130" fill="#94a3b8" font-size="8">attachments: jsonb</text>
|
||||||
|
|
||||||
|
<!-- FollowLog (property) -->
|
||||||
|
<rect x="1440" y="100" width="300" height="155" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1440" y="100" width="300" height="155" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||||
|
<line x1="1440" y1="128" x2="1740" y2="128" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="1590" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_follow_logs</text>
|
||||||
|
<text x="1450" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1450" y="158" fill="#94a3b8" font-size="8">FK property_id FK staff_id</text>
|
||||||
|
<text x="1450" y="171" fill="#94a3b8" font-size="8">log_type: call/visit/note...</text>
|
||||||
|
<text x="1450" y="184" fill="#94a3b8" font-size="8">content: text</text>
|
||||||
|
<text x="1450" y="197" fill="#94a3b8" font-size="8">sensitive_view: bool [不可删]</text>
|
||||||
|
<text x="1450" y="210" fill="#94a3b8" font-size="8">created_at, created_by</text>
|
||||||
|
<text x="1450" y="237" fill="#fb7185" font-size="7">⚠ NO DELETE (audit log)</text>
|
||||||
|
|
||||||
|
<!-- KeyManagement -->
|
||||||
|
<rect x="1440" y="490" width="300" height="120" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1440" y="490" width="300" height="120" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||||
|
<line x1="1440" y1="518" x2="1740" y2="518" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="1590" y="510" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_keys</text>
|
||||||
|
<text x="1450" y="535" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1450" y="548" fill="#94a3b8" font-size="8">FK property_id FK holder_id</text>
|
||||||
|
<text x="1450" y="561" fill="#94a3b8" font-size="8">key_no: varchar(50)</text>
|
||||||
|
<text x="1450" y="574" fill="#94a3b8" font-size="8">status: held/returned</text>
|
||||||
|
<text x="1450" y="587" fill="#94a3b8" font-size="8">taken_at, returned_at</text>
|
||||||
|
|
||||||
|
<!-- Commission -->
|
||||||
|
<rect x="1440" y="720" width="300" height="135" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1440" y="720" width="300" height="135" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||||
|
<line x1="1440" y1="748" x2="1740" y2="748" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="1590" y="740" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_commissions</text>
|
||||||
|
<text x="1450" y="765" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1450" y="778" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||||
|
<text x="1450" y="791" fill="#94a3b8" font-size="8">commission_type: exclusive/open</text>
|
||||||
|
<text x="1450" y="804" fill="#94a3b8" font-size="8">rate: numeric(5,4)</text>
|
||||||
|
<text x="1450" y="817" fill="#94a3b8" font-size="8">start_date, end_date</text>
|
||||||
|
<text x="1450" y="830" fill="#94a3b8" font-size="8">signed_at, document_key</text>
|
||||||
|
|
||||||
|
<!-- Marketing -->
|
||||||
|
<rect x="1440" y="940" width="300" height="120" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1440" y="940" width="300" height="120" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||||
|
<line x1="1440" y1="968" x2="1740" y2="968" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="1590" y="960" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_marketing</text>
|
||||||
|
<text x="1450" y="985" fill="#fbbf24" font-size="8">PK id: uuid [1:1 property]</text>
|
||||||
|
<text x="1450" y="998" fill="#94a3b8" font-size="8">FK property_id (UNIQUE)</text>
|
||||||
|
<text x="1450" y="1011" fill="#94a3b8" font-size="8">title: varchar(200)</text>
|
||||||
|
<text x="1450" y="1024" fill="#94a3b8" font-size="8">highlights: text[]</text>
|
||||||
|
<text x="1450" y="1037" fill="#94a3b8" font-size="8">published_at, platforms: jsonb</text>
|
||||||
|
|
||||||
|
<!-- ListingHistory -->
|
||||||
|
<rect x="1080" y="1250" width="300" height="120" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1080" y="1250" width="300" height="120" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||||
|
<line x1="1080" y1="1278" x2="1380" y2="1278" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="1230" y="1270" fill="white" font-size="11" font-weight="700" text-anchor="middle">listing_histories</text>
|
||||||
|
<text x="1090" y="1295" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1090" y="1308" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||||
|
<text x="1090" y="1321" fill="#94a3b8" font-size="8">listed_at, delisted_at</text>
|
||||||
|
<text x="1090" y="1334" fill="#94a3b8" font-size="8">list_price: numeric(12,2)</text>
|
||||||
|
<text x="1090" y="1347" fill="#94a3b8" font-size="8">reason: varchar(50)</text>
|
||||||
|
|
||||||
|
<!-- Arrow from Property to ListingHistory -->
|
||||||
|
<line x1="1230" y1="390" x2="1230" y2="1250" stroke="#a78bfa" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-violet)"/>
|
||||||
|
<text x="1238" y="820" fill="#a78bfa" font-size="8">1:N</text>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
<!-- CLIENT MODULE -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- Client -->
|
||||||
|
<rect x="1870" y="100" width="320" height="250" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1870" y="100" width="320" height="250" rx="6" fill="rgba(120,53,15,0.4)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||||
|
<line x1="1870" y1="128" x2="2190" y2="128" stroke="#fbbf24" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="2030" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">clients</text>
|
||||||
|
<text x="1880" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1880" y="158" fill="#94a3b8" font-size="8">FK agent_id (staff)</text>
|
||||||
|
<text x="1880" y="171" fill="#94a3b8" font-size="8">client_type: private/public/closed</text>
|
||||||
|
<text x="1880" y="184" fill="#94a3b8" font-size="8">status: active/inactive/converted</text>
|
||||||
|
<text x="1880" y="197" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||||
|
<text x="1880" y="210" fill="#94a3b8" font-size="8">phone_enc: text (AES)</text>
|
||||||
|
<text x="1880" y="223" fill="#94a3b8" font-size="8">phone_hash: varchar(64)</text>
|
||||||
|
<text x="1880" y="236" fill="#94a3b8" font-size="8">activity_level: 1-5 (Celery daily)</text>
|
||||||
|
<text x="1880" y="249" fill="#94a3b8" font-size="8">is_protected: bool [防止转公客]</text>
|
||||||
|
<text x="1880" y="262" fill="#94a3b8" font-size="8">transfer_to_public_type: auto/manual</text>
|
||||||
|
<text x="1880" y="275" fill="#94a3b8" font-size="8">source: varchar(30)</text>
|
||||||
|
<text x="1880" y="288" fill="#94a3b8" font-size="8">deleted_at, created_by</text>
|
||||||
|
<text x="1880" y="301" fill="#94a3b8" font-size="8">...</text>
|
||||||
|
<text x="1880" y="325" fill="#94a3b8" font-size="7">[私客/公客/成交客 三态状态机]</text>
|
||||||
|
|
||||||
|
<!-- ClientRequirement -->
|
||||||
|
<rect x="1960" y="490" width="280" height="155" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1960" y="490" width="280" height="155" rx="6" fill="rgba(120,53,15,0.35)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||||
|
<line x1="1960" y1="518" x2="2240" y2="518" stroke="#fbbf24" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="2100" y="510" fill="white" font-size="11" font-weight="700" text-anchor="middle">client_requirements</text>
|
||||||
|
<text x="1970" y="535" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1970" y="548" fill="#94a3b8" font-size="8">FK client_id</text>
|
||||||
|
<text x="1970" y="561" fill="#94a3b8" font-size="8">req_type: second_hand/new/rent</text>
|
||||||
|
<text x="1970" y="574" fill="#94a3b8" font-size="8">district_ids: uuid[]</text>
|
||||||
|
<text x="1970" y="587" fill="#94a3b8" font-size="8">price_min/max: numeric</text>
|
||||||
|
<text x="1970" y="600" fill="#94a3b8" font-size="8">area_min/max, bedrooms</text>
|
||||||
|
<text x="1970" y="613" fill="#94a3b8" font-size="8">school_ids: uuid[]</text>
|
||||||
|
|
||||||
|
<!-- ClientFollowLog -->
|
||||||
|
<rect x="1820" y="490" width="130" height="155" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1820" y="490" width="130" height="155" rx="6" fill="rgba(120,53,15,0.35)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||||
|
<line x1="1820" y1="518" x2="1950" y2="518" stroke="#fbbf24" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="1885" y="510" fill="white" font-size="10" font-weight="700" text-anchor="middle">client_follow_logs</text>
|
||||||
|
<text x="1828" y="535" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1828" y="548" fill="#94a3b8" font-size="8">FK client_id</text>
|
||||||
|
<text x="1828" y="561" fill="#94a3b8" font-size="8">log_type</text>
|
||||||
|
<text x="1828" y="574" fill="#94a3b8" font-size="8">content: text</text>
|
||||||
|
<text x="1828" y="587" fill="#94a3b8" font-size="8">created_at</text>
|
||||||
|
<text x="1828" y="617" fill="#fb7185" font-size="7">⚠ NO DELETE</text>
|
||||||
|
|
||||||
|
<!-- Viewing -->
|
||||||
|
<rect x="2250" y="290" width="250" height="165" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="2250" y="290" width="250" height="165" rx="6" fill="rgba(120,53,15,0.35)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||||
|
<line x1="2250" y1="318" x2="2500" y2="318" stroke="#fbbf24" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="2375" y="310" fill="white" font-size="11" font-weight="700" text-anchor="middle">client_viewings</text>
|
||||||
|
<text x="2260" y="335" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="2260" y="348" fill="#94a3b8" font-size="8">FK client_id</text>
|
||||||
|
<text x="2260" y="361" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||||
|
<text x="2260" y="374" fill="#94a3b8" font-size="8">FK agent_id (staff)</text>
|
||||||
|
<text x="2260" y="387" fill="#94a3b8" font-size="8">viewed_at: timestamptz</text>
|
||||||
|
<text x="2260" y="400" fill="#94a3b8" font-size="8">feedback: text</text>
|
||||||
|
<text x="2260" y="413" fill="#94a3b8" font-size="8">rating: smallint</text>
|
||||||
|
<text x="2260" y="426" fill="#94a3b8" font-size="8">status: planned/done/cancelled</text>
|
||||||
|
|
||||||
|
<!-- Match -->
|
||||||
|
<rect x="1960" y="700" width="280" height="150" rx="6" fill="#0f172a"/>
|
||||||
|
<rect x="1960" y="700" width="280" height="150" rx="6" fill="rgba(120,53,15,0.35)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||||
|
<line x1="1960" y1="728" x2="2240" y2="728" stroke="#fbbf24" stroke-width="0.5" opacity="0.6"/>
|
||||||
|
<text x="2100" y="720" fill="white" font-size="11" font-weight="700" text-anchor="middle">client_property_matches</text>
|
||||||
|
<text x="1970" y="745" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||||
|
<text x="1970" y="758" fill="#94a3b8" font-size="8">FK client_id</text>
|
||||||
|
<text x="1970" y="771" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||||
|
<text x="1970" y="784" fill="#94a3b8" font-size="8">match_type: system/manual</text>
|
||||||
|
<text x="1970" y="797" fill="#94a3b8" font-size="8">score: numeric(5,2)</text>
|
||||||
|
<text x="1970" y="810" fill="#94a3b8" font-size="8">status: pending/sent/viewed</text>
|
||||||
|
<text x="1970" y="823" fill="#94a3b8" font-size="8">created_at</text>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
<!-- LEGEND -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<rect x="30" y="1850" width="900" height="100" rx="8" fill="rgba(15,23,42,0.8)" stroke="#334155" stroke-width="1"/>
|
||||||
|
<text x="50" y="1873" fill="#94a3b8" font-size="9" font-weight="600">LEGEND</text>
|
||||||
|
|
||||||
|
<!-- Org -->
|
||||||
|
<rect x="50" y="1885" width="14" height="14" rx="2" fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||||
|
<text x="70" y="1896" fill="#22d3ee" font-size="8">ORG / HR</text>
|
||||||
|
|
||||||
|
<!-- Complex -->
|
||||||
|
<rect x="155" y="1885" width="14" height="14" rx="2" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||||
|
<text x="175" y="1896" fill="#34d399" font-size="8">REGION & COMPLEX</text>
|
||||||
|
|
||||||
|
<!-- Property -->
|
||||||
|
<rect x="310" y="1885" width="14" height="14" rx="2" fill="rgba(76,29,149,0.4)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||||
|
<text x="330" y="1896" fill="#a78bfa" font-size="8">PROPERTY</text>
|
||||||
|
|
||||||
|
<!-- Client -->
|
||||||
|
<rect x="420" y="1885" width="14" height="14" rx="2" fill="rgba(120,53,15,0.4)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||||
|
<text x="440" y="1896" fill="#fbbf24" font-size="8">CLIENT</text>
|
||||||
|
|
||||||
|
<!-- Relationship lines -->
|
||||||
|
<line x1="50" y1="1920" x2="90" y2="1920" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arrow-slate)"/>
|
||||||
|
<text x="96" y="1924" fill="#94a3b8" font-size="8">Foreign Key (FK)</text>
|
||||||
|
|
||||||
|
<line x1="210" y1="1920" x2="250" y2="1920" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-slate)"/>
|
||||||
|
<text x="256" y="1924" fill="#94a3b8" font-size="8">Soft reference / optional FK</text>
|
||||||
|
|
||||||
|
<rect x="410" y="1913" width="14" height="14" rx="2" fill="rgba(15,23,42,0.5)" stroke="#34d399" stroke-width="1" stroke-dasharray="3,2"/>
|
||||||
|
<text x="430" y="1924" fill="#34d399" font-size="8">Join table (N:M)</text>
|
||||||
|
|
||||||
|
<text x="550" y="1924" fill="#fbbf24" font-size="8">PK Primary Key</text>
|
||||||
|
<text x="640" y="1924" fill="#fb7185" font-size="8">⚠ NO DELETE = append-only audit log</text>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
<!-- TITLE BLOCK -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
<text x="1080" y="1930" fill="#475569" font-size="10" text-anchor="middle">Fonrey 房产经纪管理系统 — Entity Relationship Diagram · v1.0 · 2026-04-24 · Schema-per-Tenant (django-tenants)</text>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 40 KiB |
BIN
Project/fonrey/DATA_MODEL/diagram/fonrey-er@2x.png
Normal file
BIN
Project/fonrey/DATA_MODEL/diagram/fonrey-er@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 869 KiB |
@@ -1,28 +1,44 @@
|
|||||||
---
|
---
|
||||||
title: "GitOps"
|
title: "GitOps"
|
||||||
type: concept
|
type: concept
|
||||||
tags: [devops, gitops, infrastructure, git]
|
tags:
|
||||||
sources: [devops-culture-and-transformation-fostering-collaboration-agile-practices-and-innovation-linkedin, ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]
|
- DevOps
|
||||||
last_updated: 2026-04-22
|
- CI/CD
|
||||||
|
- Kubernetes
|
||||||
|
- Infrastructure as Code
|
||||||
---
|
---
|
||||||
|
|
||||||
## Summary
|
## Definition
|
||||||
GitOps is a DevOps methodology that uses Git as the single source of truth for managing infrastructure and application deployments. All desired state is stored in Git repositories, and automated tools (like ArgoCD or Flux) continuously reconcile the actual cluster state with the desired state defined in Git. It is identified as a key future trend in DevOps for managing both infrastructure and deployments declaratively.
|
GitOps 是一种将软件开发的版本控制与协作原则应用于云原生基础设施和应用部署的方法论。核心思想:**使用 Git 作为单一事实来源(Single Source of Truth)声明系统的期望状态,由自动化代理(GitOps Controller)持续协调实际状态向期望状态收敛。**
|
||||||
|
|
||||||
## Key Concepts
|
## Four Principles
|
||||||
|
1. **声明式配置(Declarative Configuration)**:所有基础设施和应用配置必须以声明式代码描述,而非命令式脚本
|
||||||
|
2. **版本控制(Version Control)**:所有配置存储在 Git 仓库中,享受完整的变更历史、代码审查和回滚能力
|
||||||
|
3. **CD 流程分离(CD Process Separation)**:CI 专注构建和分析代码,CD 专注部署,两者解耦增强安全性
|
||||||
|
4. **自修复协调器(Automated Reconciliation)**:GitOps Controller 持续监控实际状态与 Git 声明状态,自动调和偏差
|
||||||
|
|
||||||
### Core Principles
|
## Key Benefits
|
||||||
1. **The entire system described declaratively** — All infrastructure and application configurations are stored as code
|
- 开发者只需掌握 Git 即可完成安全部署
|
||||||
2. **The canonical desired state in Git** — Git is the source of truth; any change goes through Git workflow
|
- 分钟级代码变更上线
|
||||||
3. **Approved changes automatically pulled into the system** — Automated agents detect drift and reconcile
|
- 零停机回滚(Git 历史即回滚计划)
|
||||||
|
- Git 提交日志天然构成合规审计追踪
|
||||||
|
- 提高开发者生产力(使用熟悉的工具)
|
||||||
|
|
||||||
### Tools
|
## Pull Model vs Push Model
|
||||||
- **ArgoCD** — Kubernetes-native GitOps controller
|
|
||||||
- **Flux** — GitOps toolkit for Kubernetes
|
|
||||||
- **Atlantis** — Terraform GitOps automation (mentioned in CTP topics)
|
|
||||||
|
|
||||||
## Connections
|
| | Pull Model(推荐) | Push Model |
|
||||||
- [[DevOps Culture]] — GitOps is an operational pattern emerging from DevOps culture
|
|---|---|---|
|
||||||
- [[Infrastructure as Code (IaC)]] — GitOps extends IaC with Git-centric workflows
|
| 机制 | 部署代理主动监控 Git 和目标系统 | CI/CD 管道主动推送变更到目标 |
|
||||||
- [[CI/CD Pipeline]] — GitOps can be considered a specialized CI/CD pattern
|
| 安全性 | 更高——系统状态不暴露给外部 | 较低——需外部访问目标系统 |
|
||||||
- [[Continuous Improvement (Kaizen)]] — GitOps enables continuous, auditable improvements
|
| 代表工具 | ArgoCD, Flux | Jenkins CI/CD, Terraform Cloud |
|
||||||
|
| 适用场景 | GitOps 核心模式 | 传统 CI/CD 扩展 |
|
||||||
|
|
||||||
|
## Relationship with IaC
|
||||||
|
GitOps 是 [[Infrastructure as Code]] 的部署编排层:
|
||||||
|
- **IaC**:定义"基础设施应该是什么样的"(Terraform/Pulumi/HCL)
|
||||||
|
- **GitOps**:定义"如何确保基础设施始终符合声明"(ArgoCD/Flux/Atlantis)
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
- [[ctp-topic-33-an-introduction-to-gitops]] — GitOps 方法论入门,Victor Etkin 主讲
|
||||||
|
- [[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]] — Atlantis 作为 GitOps 工具实践
|
||||||
|
- [[ctp-topic-9-ci-cd-with-gruntwork]] — Gruntwork CI/CD 与 GitOps 的关联
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
- [Overview](overview.md) — living synthesis
|
- [Overview](overview.md) — living synthesis
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
- [2026-04-24] [Public Cloud Learning Sessions - Ollie Workflow and The Demand Process - 20240416](sources/public-cloud-learning-sessions-ollie-workflow-and-the-demand-process-20240416-16.md)
|
||||||
|
- [2026-04-24] [CTP Topic 33 An Introduction to GitOps](sources/ctp-topic-33-an-introduction-to-gitops.md)
|
||||||
|
- [2026-04-24] [CTP Topic 3 Deploy and maintain infrastructure](sources/ctp-topic-3-deploy-and-maintain-infrastructure.md)
|
||||||
- [2026-04-24] [CTP Topic 9 CI CD with Gruntwork](sources/ctp-topic-9-ci-cd-with-gruntwork.md)
|
- [2026-04-24] [CTP Topic 9 CI CD with Gruntwork](sources/ctp-topic-9-ci-cd-with-gruntwork.md)
|
||||||
- [2026-04-24] [CTP Topic 32 Using Atlantis CICD for Infrastructure Deployments](sources/ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments.md)
|
- [2026-04-24] [CTP Topic 32 Using Atlantis CICD for Infrastructure Deployments](sources/ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments.md)
|
||||||
- [2026-04-24] [CTP Topic 2 Git](sources/ctp-topic-2-git.md)
|
- [2026-04-24] [CTP Topic 2 Git](sources/ctp-topic-2-git.md)
|
||||||
@@ -411,9 +414,6 @@
|
|||||||
- [2026-04-19] [ctp-topic-13-cloud-finops-micro-focus-policies-best-practices-to-optimize-the-co](sources/ctp-topic-13-cloud-finops-micro-focus-policies-best-practices-to-optimize-the-co.md) — (expected: wiki/sources/ctp-topic-13-cloud-finops-micro-focus-policies-best-practices-to-optimize-the-co.md — source missing)
|
- [2026-04-19] [ctp-topic-13-cloud-finops-micro-focus-policies-best-practices-to-optimize-the-co](sources/ctp-topic-13-cloud-finops-micro-focus-policies-best-practices-to-optimize-the-co.md) — (expected: wiki/sources/ctp-topic-13-cloud-finops-micro-focus-policies-best-practices-to-optimize-the-co.md — source missing)
|
||||||
- [2026-04-19] [ctp-topic-15-working-with-renovatebot](sources/ctp-topic-15-working-with-renovatebot.md) — (expected: wiki/sources/ctp-topic-15-working-with-renovatebot.md — source missing)
|
- [2026-04-19] [ctp-topic-15-working-with-renovatebot](sources/ctp-topic-15-working-with-renovatebot.md) — (expected: wiki/sources/ctp-topic-15-working-with-renovatebot.md — source missing)
|
||||||
- [2026-04-19] [ctp-topic-56-automated-infrastructure-testing](sources/ctp-topic-56-automated-infrastructure-testing.md) — (expected: wiki/sources/ctp-topic-56-automated-infrastructure-testing.md — source missing)
|
- [2026-04-19] [ctp-topic-56-automated-infrastructure-testing](sources/ctp-topic-56-automated-infrastructure-testing.md) — (expected: wiki/sources/ctp-topic-56-automated-infrastructure-testing.md — source missing)
|
||||||
- [2026-04-19] [public-cloud-learning-sessions-ollie-workflow-and-the-demand-process-20240416-16](sources/public-cloud-learning-sessions-ollie-workflow-and-the-demand-process-20240416-16.md) — (expected: wiki/sources/public-cloud-learning-sessions-ollie-workflow-and-the-demand-process-20240416-16.md — source missing)
|
|
||||||
- [2026-04-19] [ctp-topic-33-an-introduction-to-gitops](sources/ctp-topic-33-an-introduction-to-gitops.md) — (expected: wiki/sources/ctp-topic-33-an-introduction-to-gitops.md — source missing)
|
|
||||||
- [2026-04-19] [ctp-topic-3-deploy-and-maintain-infrastructure](sources/ctp-topic-3-deploy-and-maintain-infrastructure.md) — (expected: wiki/sources/ctp-topic-3-deploy-and-maintain-infrastructure.md — source missing)
|
|
||||||
- [Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog](sources/Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog.md) — (expected: wiki/sources/Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog.md — source missing)
|
- [Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog](sources/Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog.md) — (expected: wiki/sources/Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog.md — source missing)
|
||||||
- [Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend](sources/Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend.md) — (expected: wiki/sources/Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend.md — source missing)
|
- [Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend](sources/Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend.md) — (expected: wiki/sources/Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend.md — source missing)
|
||||||
- [zk-steward](sources/zk-steward.md) — (expected: wiki/sources/zk-steward.md — source missing)
|
- [zk-steward](sources/zk-steward.md) — (expected: wiki/sources/zk-steward.md — source missing)
|
||||||
|
|||||||
35
wiki/log.md
35
wiki/log.md
@@ -1,3 +1,22 @@
|
|||||||
|
## [2026-04-24] ingest | Public Cloud Learning Sessions - Ollie Workflow and The Demand Process - 20240416
|
||||||
|
- Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/public-cloud-learning-sessions-ollie-workflow-and-the-demand-process-20240416-16.md
|
||||||
|
- Status: ✅ 成功摄入
|
||||||
|
- Summary: Oli 工作流(超大规模云厂商支出审批三级工作流)+ 需求管理自动化端到端流程(ITIL 框架、Octane/Qixi 提交入口、主服务目录嵌入 SMACs、"机器做机器能做的事"理念)
|
||||||
|
- Concepts identified: [[Demand-Management]], [[ITIL-Service-Management]], [[FinOps]], [[SMACs]]
|
||||||
|
- Entities identified: [[Tom-Bice]], [[FPNA-Team]], [[MUI]], [[Shannon]], [[Octane]], [[Qixi]]
|
||||||
|
- Source page: wiki/sources/public-cloud-learning-sessions-ollie-workflow-and-the-demand-process-20240416-16.md
|
||||||
|
- Notes: entities 和 concepts 目录均为空(无历史页面);未满足 ≥2 次出现条件,不新建独立页面,以 wikilink 形式记录于 Source page;index.md 已更新;overview.md Cloud Transformation 章节已补充(置于 ctp-topic-57 后);已建立与 ctp-topic-57(Backlog 管理管道)、ctp-topic-65(价值量化)、public-cloud-learning-sessions-applicable-business-analysis-techniques-20240109(需求分析前置技法)、ctp-topic-4(敏捷实践)的连接关系
|
||||||
|
- Conflicts: (暂无)
|
||||||
|
|
||||||
|
- Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/ctp-topic-3-deploy-and-maintain-infrastructure.md
|
||||||
|
- Status: ✅ 成功摄入
|
||||||
|
- Summary: Landing Zone 环境下通过 Terraform/Terragrunt 实现基础设施部署与维护的完整方法论;核心区分 Service Module(业务视角)与 Regular Module(技术视角)的分层抽象;Terragrunt HCL 版本锁定;Service Catalog 三级复用(单账户→产品团队→跨团队)
|
||||||
|
- Concepts identified: [[Service Module]], [[Service Catalog]], [[Terragrunt]], [[Infrastructure as Code]], [[Terraform Module]]
|
||||||
|
- Entities identified: [[Gruntwork]], [[AWS Landing Zone]]
|
||||||
|
- Source page: wiki/sources/ctp-topic-3-deploy-and-maintain-infrastructure.md
|
||||||
|
- Notes: 已建立与 ctp-topic-1(Gruntwork LZ 架构)、ctp-topic-9(CI/CD with Gruntwork)、ctp-topic-32(Atlantis CI/CD)、ctp-topic-33(GitOps 入门)、ctp-topic-39(EKS Atlantis 约束差异)的连接关系;Service Module/Service Catalog 仅出现 1 次,不满足 ≥2 次建页条件,以 wikilink 形式记录于 Source page;index.md 已更新;overview.md Cloud Transformation & DevOps 章节已更新
|
||||||
|
- Conflicts: 与 [[ctp-topic-39-implementing-eks-in-the-aws-lab-landing-zone]] 存在 Atlantis EKS 支持约束差异(Topic 3 通用原则 vs Topic 39 具体实践)
|
||||||
|
|
||||||
## [2026-04-24] ingest | CTP Topic 9 CI CD with Gruntwork
|
## [2026-04-24] ingest | CTP Topic 9 CI CD with Gruntwork
|
||||||
- Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/ctp-topic-9-ci-cd-with-gruntwork.md
|
- Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/ctp-topic-9-ci-cd-with-gruntwork.md
|
||||||
- Status: ✅ 成功摄入
|
- Status: ✅ 成功摄入
|
||||||
@@ -2354,3 +2373,19 @@
|
|||||||
- 新增 Entity 页面:Jay-Comer.md
|
- 新增 Entity 页面:Jay-Comer.md
|
||||||
- 新增 Concept 页面:OpenTelemetry.md
|
- 新增 Concept 页面:OpenTelemetry.md
|
||||||
- 冲突检测:与 ctp-topic-54-esm-saas-log-analytics(ELK 日志)、ctp-topic-67(CTP Topic 67 OpenTelemetry)互补,无冲突
|
- 冲突检测:与 ctp-topic-54-esm-saas-log-analytics(ELK 日志)、ctp-topic-67(CTP Topic 67 OpenTelemetry)互补,无冲突
|
||||||
|
|
||||||
|
## [2026-04-25] ingest | CTP Topic 33 An Introduction to GitOps
|
||||||
|
- Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/ctp-topic-33-an-introduction-to-gitops.md
|
||||||
|
- Status: ✅ 成功摄入
|
||||||
|
- Summary: GitOps 方法论入门——将软件开发原则应用于部署流程;四大原则(声明式配置 + 版本控制 + CD 流程分离 + 自修复协调器);Pull 模型优于 Push 模型;幂等平台(Kubernetes)是 CD 顺利运行的必要条件;Git 提交日志即合规审计追踪
|
||||||
|
- Concepts created: [[GitOps]]
|
||||||
|
- Entities identified: [[Victor Etkin]]
|
||||||
|
- Source page: wiki/sources/ctp-topic-33-an-introduction-to-gitops.md
|
||||||
|
- Notes:
|
||||||
|
- 新增 1 个 Source Page
|
||||||
|
- 新增 1 个 Concept 页面:GitOps.md(覆盖四大原则、Pull vs Push 模型、与 IaC 关系)
|
||||||
|
- index.md 更新:新增条目于 CI_CD_GitOps 分类
|
||||||
|
- overview.md 更新:新增条目于 Cloud Transformation & DevOps 章节,GitOps 知识链路
|
||||||
|
- Key Entities 中提及的 Victor Etkin 仅出现 1 次,不满足 ≥2 次条件,以 wikilink 形式记录于 Source page
|
||||||
|
- Key Concepts 中 Kubernetes/Atlantis 已有 wikilink 指向其他 Source page
|
||||||
|
- 冲突检测:与 ctp-topic-39(Atlantis 不支持 EKS)存在 Atlantis + Kubernetes 实践约束差异,已记录于 Source page Contradictions
|
||||||
|
|||||||
@@ -39,12 +39,16 @@ Key concepts: [[Recursive Self-Optimization]], [[Generator Space]], [[Self-Refer
|
|||||||
**[[multi-source-tech-news-digest]]**:AI Agent 驱动的多源科技新闻自动聚合与投递系统——四层数据管道整合 46 个 RSS 源、44 个 Twitter/X KOL 账号、19 个 GitHub Releases 仓库和 4 个 Brave Search 主题,覆盖 109+ 信息源;通过标题相似度去重和多维度质量评分(priority source +3, multi-source +5, recency +2, engagement +1)生成精选简报;支持 Discord/Email/Telegram 三通道投递,30 秒内通过自然语言添加自定义来源。属 [[Daily-YouTube-Digest]] / [[Daily Reddit Digest]] 同款 Cron Job + AI 摘要模式的不同垂直场景(前者视频,后者 Reddit 社区,本方案文字新闻)。
|
**[[multi-source-tech-news-digest]]**:AI Agent 驱动的多源科技新闻自动聚合与投递系统——四层数据管道整合 46 个 RSS 源、44 个 Twitter/X KOL 账号、19 个 GitHub Releases 仓库和 4 个 Brave Search 主题,覆盖 109+ 信息源;通过标题相似度去重和多维度质量评分(priority source +3, multi-source +5, recency +2, engagement +1)生成精选简报;支持 Discord/Email/Telegram 三通道投递,30 秒内通过自然语言添加自定义来源。属 [[Daily-YouTube-Digest]] / [[Daily Reddit Digest]] 同款 Cron Job + AI 摘要模式的不同垂直场景(前者视频,后者 Reddit 社区,本方案文字新闻)。
|
||||||
|
|
||||||
### Cloud Transformation & DevOps
|
### Cloud Transformation & DevOps
|
||||||
Git 是云转型计划中 DevOps 与 CI/CD 流水线的基础技能。**[[ctp-topic-2-git]]**(CTP Topic 2)作为 CI/CD/GitOps 系列的开篇,涵盖 Git 版本控制系统基础概念与实践,与 [[ctp-topic-9-ci-cd-with-gruntwork]](Gruntwork CI/CD)和 [[ctp-topic-33-an-introduction-to-gitops]](GitOps 入门)构成完整的学习链路。**[[ctp-topic-9-ci-cd-with-gruntwork]]**(CTP Topic 9)聚焦 CI/CD 与 Gruntwork 在 AWS Landing Zone 中的实践,基于 Gruntwork 参考架构通过 Terraform/Terragrunt 实现基础设施自动化交付(⚠️ 视频待 Whisper 转录后补充详细内容)。
|
Git 是云转型计划中 DevOps 与 CI/CD 流水线的基础技能。**[[ctp-topic-2-git]]**(CTP Topic 2)作为 CI/CD/GitOps 系列的开篇,涵盖 Git 版本控制系统基础概念与实践,与 [[ctp-topic-9-ci-cd-with-gruntwork]](Gruntwork CI/CD)和 [[ctp-topic-33-an-introduction-to-gitops]](GitOps 入门)构成完整的学习链路。**[[ctp-topic-3-deploy-and-maintain-infrastructure]]**(CTP Topic 3)深入 Landing Zone 环境下的基础设施部署方法论——核心区分:Service Module(业务视角,满足业务需求的一组模块组合)与 Regular Module(技术视角,单一技术构建块);Terragrunt HCL 文件通过版本锁定而非 master 分支引用模块;Service Catalog 支持三级复用(单账户→产品团队→跨团队)。类 OO 继承原则:抽象层次越高,配置选项越少。Terragrunt 在运行前预取所有依赖,通过缓存目录管理克隆仓库。属 IaC 模块化治理的基础原则层,与 [[ctp-topic-9-ci-cd-with-gruntwork]](CI/CD 实践)和 [[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]](Atlantis 工具)共同构成完整的 IaC 知识链路。注意:[[ctp-topic-39-implementing-eks-in-the-aws-lab-landing-zone]] 提到 Atlantis 当前不支持 EKS 部署,两者存在实践约束差异,需通过 Jenkins + Terragrunt 替代。
|
||||||
|
|
||||||
|
**[[ctp-topic-9-ci-cd-with-gruntwork]]**(CTP Topic 9)聚焦 CI/CD 与 Gruntwork 在 AWS Landing Zone 中的实践,基于 Gruntwork 参考架构通过 Terraform/Terragrunt 实现基础设施自动化交付(⚠️ 视频待 Whisper 转录后补充详细内容)。
|
||||||
|
|
||||||
Cloud Transformation Programme (CTP) materials cover AWS landing zones, EKS, Terraform, GitOps, FinOps, observability, security, and enterprise architecture. Key themes: 3 Lines of Defence framework, ITSM, container hardening, backup & DR strategies. DevOps culture focuses on four pillars: Collaboration, Automation (CI/CD, IaC), Continuous Improvement (Kaizen), and Customer-Centricity. Agile practices (Scrum, Kanban) are symbiotic with DevOps. Emerging trends: DevSecOps, GitOps, Serverless DevOps, AI/ML-driven automation, and Edge Computing DevOps.
|
Cloud Transformation Programme (CTP) materials cover AWS landing zones, EKS, Terraform, GitOps, FinOps, observability, security, and enterprise architecture. Key themes: 3 Lines of Defence framework, ITSM, container hardening, backup & DR strategies. DevOps culture focuses on four pillars: Collaboration, Automation (CI/CD, IaC), Continuous Improvement (Kaizen), and Customer-Centricity. Agile practices (Scrum, Kanban) are symbiotic with DevOps. Emerging trends: DevSecOps, GitOps, Serverless DevOps, AI/ML-driven automation, and Edge Computing DevOps.
|
||||||
|
|
||||||
**[[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]]**(CTP Topic 32):Atlantis 替代 Jenkins 用于 Terraform IaC 部署——针对当前 Jenkins 流水线初始化慢(多次代码克隆/顺序测试/ECS 预配置)和架构复杂(持续叠加功能导致脆弱)的双重痛点,Atlantis 提供 PR 评论式协作模型,开发者直接在 GitHub PR 上评论 `atlantis plan`/`apply` 即可触发变更,无需独立账号;每个 Landing Zone 共享账户部署单台 EC2 实例,通过 GitHub Enterprise Webhook 接收通知,服务账号负责评论/合并/关闭 PR;跨账户访问通过在各账户部署的 IAM 角色实现;并行构建支持多模块并发 plan/apply;锁定机制防止多 PR 同时操作同一模块产生冲突。Atlantis 在 merge 前即应用变更,确保代码与基础设施始终同步。属 [[GitOps]] 工具实践层,与 [[ctp-topic-33-an-introduction-to-gitops]](GitOps 概念)和 [[ctp-topic-9-ci-cd-with-gruntwork]](Gruntwork CI/CD)共同构成完整链路。注意:[[ctp-topic-39-implementing-eks-in-the-aws-lab-landing-zone]] 提到 Atlantis 当前不支持 EKS 部署,两者存在实践约束差异。
|
**[[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]]**(CTP Topic 32):Atlantis 替代 Jenkins 用于 Terraform IaC 部署——针对当前 Jenkins 流水线初始化慢(多次代码克隆/顺序测试/ECS 预配置)和架构复杂(持续叠加功能导致脆弱)的双重痛点,Atlantis 提供 PR 评论式协作模型,开发者直接在 GitHub PR 上评论 `atlantis plan`/`apply` 即可触发变更,无需独立账号;每个 Landing Zone 共享账户部署单台 EC2 实例,通过 GitHub Enterprise Webhook 接收通知,服务账号负责评论/合并/关闭 PR;跨账户访问通过在各账户部署的 IAM 角色实现;并行构建支持多模块并发 plan/apply;锁定机制防止多 PR 同时操作同一模块产生冲突。Atlantis 在 merge 前即应用变更,确保代码与基础设施始终同步。属 [[GitOps]] 工具实践层,与 [[ctp-topic-33-an-introduction-to-gitops]](GitOps 概念)和 [[ctp-topic-9-ci-cd-with-gruntwork]](Gruntwork CI/CD)共同构成完整链路。注意:[[ctp-topic-39-implementing-eks-in-the-aws-lab-landing-zone]] 提到 Atlantis 当前不支持 EKS 部署,两者存在实践约束差异。
|
||||||
|
|
||||||
|
**[[ctp-topic-33-an-introduction-to-gitops]]**(CTP Topic 33):Victor Etkin 讲解 GitOps 方法论入门——GitOps 将软件开发原则应用于部署流程,解决部署失败和配置不一致问题。四大原则:声明式配置、版本控制、CD 流程分离、自修复协调器;核心工具仅需 Git。GitOps Controller 持续比对 Git 声明的期望状态与系统实际状态,自动调和偏差。Pull 模型(代理同时监控 Git 和目标系统)比 Push 模型安全性更高,是 GitOps 推荐模式。CI 专注代码构建和分析,CD 专注二进制部署,两者解耦增强安全性。幂等平台(如 Kubernetes)是 CD 流程顺利运行的必要条件。Git 提交日志天然构成合规审计追踪。属 [[GitOps]] 概念层核心来源,与 [[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]](Atlantis 工具)和 [[ctp-topic-2-git]](Git 基础)共同构成 CI/CD/GitOps 完整知识链路。
|
||||||
|
|
||||||
**[[ctp-topic-21-supply-chain-security-in-micro-focus]]**(CTP Topic 21):Micro Focus 产品安全小组 Shlomi Ben-Hur 主讲的软件供应链安全新方法——核心议题:在云转型背景下,软件供应链安全已成为企业安全战略的重中之重。供应链(产品层面)涵盖源码管理(SCM)、构建组件(CI)、制品库到最终交付系统(CD)的所有环节,Micro Focus 内部存在 17 种不同 SCM 工具的极高多样性。主要驱动因素:SolarWinds 攻击事件(通过渗透构建过程注入恶意代码)、美国网络安全行政命令、以及向 AWS/SaaS 迁移带来的开放性风险。核心转变:从过去 99% 关注研发安全(代码扫描/渗透测试)转向全生命周期安全防护;供应链安全成为 SDL(安全开发生命周期)的第五大支柱,强调必须同时确保 CI 过程(构建环境/自动化服务器)和 CD 过程(交付系统)的完整性,防止黑客在任何环节篡改二进制文件。属 [[Supply Chain Security(供应链安全)]] 在 [[Micro Focus]] 云转型场景的核心实践,与 [[DevSecOps]](开发安全运维一体化)高度关联。
|
**[[ctp-topic-21-supply-chain-security-in-micro-focus]]**(CTP Topic 21):Micro Focus 产品安全小组 Shlomi Ben-Hur 主讲的软件供应链安全新方法——核心议题:在云转型背景下,软件供应链安全已成为企业安全战略的重中之重。供应链(产品层面)涵盖源码管理(SCM)、构建组件(CI)、制品库到最终交付系统(CD)的所有环节,Micro Focus 内部存在 17 种不同 SCM 工具的极高多样性。主要驱动因素:SolarWinds 攻击事件(通过渗透构建过程注入恶意代码)、美国网络安全行政命令、以及向 AWS/SaaS 迁移带来的开放性风险。核心转变:从过去 99% 关注研发安全(代码扫描/渗透测试)转向全生命周期安全防护;供应链安全成为 SDL(安全开发生命周期)的第五大支柱,强调必须同时确保 CI 过程(构建环境/自动化服务器)和 CD 过程(交付系统)的完整性,防止黑客在任何环节篡改二进制文件。属 [[Supply Chain Security(供应链安全)]] 在 [[Micro Focus]] 云转型场景的核心实践,与 [[DevSecOps]](开发安全运维一体化)高度关联。
|
||||||
|
|
||||||
**[[ctp-topic-24-micro-focus-product-privacy-framework]]**(CTP Topic 24):Micro Focus 产品隐私框架在云转型中的应用——PSAC(产品安全顾问委员会)与法律顾问合作,将 GDPR/CCPA 等晦涩法律条款翻译为约 110 项低级别技术要求;隐私框架是 STLC(安全开发生命周期)中 13 个安全与隐私轨道之一;通过五类需求(架构类/文档类/法律类/实现类/SAS 运营类)和成熟度模型(0-4 级)评估产品隐私合规状态;通过"蜘蛛图"直观展示产品在安全去标识化、被遗忘权、数据可移植性等 KPI 上的合规现状;最终产出标准化《产品隐私设置文档》,确保客户获得一致的隐私信息参考。属 [[Product Privacy Framework(产品隐私框架)]] 在 [[Micro Focus]] 云转型场景的核心实践,与 [[Micro Focus Security Development Life Cycle (STLC) Overview]](STLC 整体架构)直接关联。
|
**[[ctp-topic-24-micro-focus-product-privacy-framework]]**(CTP Topic 24):Micro Focus 产品隐私框架在云转型中的应用——PSAC(产品安全顾问委员会)与法律顾问合作,将 GDPR/CCPA 等晦涩法律条款翻译为约 110 项低级别技术要求;隐私框架是 STLC(安全开发生命周期)中 13 个安全与隐私轨道之一;通过五类需求(架构类/文档类/法律类/实现类/SAS 运营类)和成熟度模型(0-4 级)评估产品隐私合规状态;通过"蜘蛛图"直观展示产品在安全去标识化、被遗忘权、数据可移植性等 KPI 上的合规现状;最终产出标准化《产品隐私设置文档》,确保客户获得一致的隐私信息参考。属 [[Product Privacy Framework(产品隐私框架)]] 在 [[Micro Focus]] 云转型场景的核心实践,与 [[Micro Focus Security Development Life Cycle (STLC) Overview]](STLC 整体架构)直接关联。
|
||||||
@@ -155,6 +159,8 @@ Key concepts: [[Process]], [[Value]], [[Value-Stream]], [[Value-Adding]], [[Wast
|
|||||||
|
|
||||||
**[[ctp-topic-57-product-backlog-managing-demand]]**(CTP Topic 57):CTP 产品待办列表(Backlog)需求管理完整管道——①需求提交(通过 SMACs 启动计时器和追踪)→ ②双周评审会议(Matthew Chapman/David Grant/Brendan)评估理解度、价值和优先级,约20题评估问卷判断简洁性、成本和野心程度 → ③Octane 特性化(带任务列表)→ ④Sprint 规划(提前6个 Sprint,50% 新需求 / 50% 支持+技术债)→ ⑤Prerequisite Phase(新产品组入职:介绍会议→AWS账户创建→解决方案设计→GitHub仓库→防火墙标签,产品团队约2小时,1-2周)→ ⑥SRE 构建账号并交接(提供控制台/GitHub访问详情)→ ⑦2周 Hyper Care 支持。现有产品组通过 SMACs/邮件/Teams 请求支持,Teams 频道连接产品组、SRE工程师、解决方案架构师和交付经理。核心理念:**透明化需求管道,确保所有工作以同一标准评估**。属 [[AWS-Landing-Zone]] 需求治理层的核心补充,与 [[ctp-topic-20-program-demand-process-flow]](Gate Process 和 POC 入职)、[[ctp-topic-4-using-agile-to-run-the-cloud-transformation-program]](敏捷实践)、[[ctp-topic-30-managing-change]](变更管理与 SRE 协作)共同构成完整的 CTP 治理知识体系。
|
**[[ctp-topic-57-product-backlog-managing-demand]]**(CTP Topic 57):CTP 产品待办列表(Backlog)需求管理完整管道——①需求提交(通过 SMACs 启动计时器和追踪)→ ②双周评审会议(Matthew Chapman/David Grant/Brendan)评估理解度、价值和优先级,约20题评估问卷判断简洁性、成本和野心程度 → ③Octane 特性化(带任务列表)→ ④Sprint 规划(提前6个 Sprint,50% 新需求 / 50% 支持+技术债)→ ⑤Prerequisite Phase(新产品组入职:介绍会议→AWS账户创建→解决方案设计→GitHub仓库→防火墙标签,产品团队约2小时,1-2周)→ ⑥SRE 构建账号并交接(提供控制台/GitHub访问详情)→ ⑦2周 Hyper Care 支持。现有产品组通过 SMACs/邮件/Teams 请求支持,Teams 频道连接产品组、SRE工程师、解决方案架构师和交付经理。核心理念:**透明化需求管道,确保所有工作以同一标准评估**。属 [[AWS-Landing-Zone]] 需求治理层的核心补充,与 [[ctp-topic-20-program-demand-process-flow]](Gate Process 和 POC 入职)、[[ctp-topic-4-using-agile-to-run-the-cloud-transformation-program]](敏捷实践)、[[ctp-topic-30-managing-change]](变更管理与 SRE 协作)共同构成完整的 CTP 治理知识体系。
|
||||||
|
|
||||||
|
**[[public-cloud-learning-sessions-ollie-workflow-and-the-demand-process-20240416-16]]**(Public Cloud Learning Sessions,Oli Workflow):超大规模云厂商支出审批工作流与需求管理端到端流程——涵盖两大部分:① **Oli 工作流审批机制**:所有超大规模云厂商支出无论金额均需 MUI 或 Shannon 书面审批;Oli 系统由 Tom Bice 领导的 FinOps 团队接管,正在集成到 SMACs 平台;提议的三阶段审批工作流(FinOps 可行性验证→云服务技术可行性验证→FPNA 团队预算可用性验证);Oli 系统提供飞行中 CSV 报告追踪状态/申请人/成本中心/月成本。② **OpenText 需求管理全链路**:ITIL 框架下服务战略→设计→过渡→运营→持续改进五阶段;主服务目录(Combined Cloud Products Master Catalog)将嵌入 SMACs;Octane 和 Qixi 是两大需求提交入口;ADM/ITOM 需求规划会议捕获所需内容、数量和发布版本;核心理念:**"机器做机器能做的事",目标 80% 场景业务单元自助完成需求选择**。属 [[Demand-Management]] 和 [[FinOps]] 在 OpenText 云转型场景的核心实践,与 [[ctp-topic-57-product-backlog-managing-demand]](Backlog 管理管道)共同构成完整的需求治理知识体系。
|
||||||
|
|
||||||
**[[ctp-topic-65-tracing-the-value-delivered-in-cloud-transformation]]**(CTP Topic 65):云转型中的价值交付量化框架——提供系统性衡量、捕获和优先排序云转型业务价值的方法论。核心内容:①基础概念——过程(Process)将输入转化为产出/成果,成果分硬性(时间/成本/质量)和软性(健康/安全);Lean 识别三类活动:增值活动、价值赋能活动、浪费;价值(Value)由客户决定,体现为公平回报;②价值流(Value Stream)分为运营价值流(OVS,面向客户)和开发价值流(DVS,内部产品);③收益量化框架——涵盖财务、生产力、质量和体验四个维度,聚焦收入增长、成本降低、风险改善和服务可获得市场规模(SOM);④WSJF 优先级排序——通过 Cost of Delay / Size of Job 比值对工作排序,实现"最小投入尽早交付最大价值";延迟成本 = 业务价值 + 时间紧迫性 + 风险与机会;⑤功能级价值拆解——可按单一功能归属、均分或不均匀分配(基于触达/影响/努力等标准)。属 [[AWS-Landing-Zone]] 价值治理层的核心方法论,与 [[ctp-topic-30-managing-change]](变更管理)和 [[ctp-topic-20-program-demand-process-flow]](需求流程)共同构成完整的 CTP 治理知识体系。
|
**[[ctp-topic-65-tracing-the-value-delivered-in-cloud-transformation]]**(CTP Topic 65):云转型中的价值交付量化框架——提供系统性衡量、捕获和优先排序云转型业务价值的方法论。核心内容:①基础概念——过程(Process)将输入转化为产出/成果,成果分硬性(时间/成本/质量)和软性(健康/安全);Lean 识别三类活动:增值活动、价值赋能活动、浪费;价值(Value)由客户决定,体现为公平回报;②价值流(Value Stream)分为运营价值流(OVS,面向客户)和开发价值流(DVS,内部产品);③收益量化框架——涵盖财务、生产力、质量和体验四个维度,聚焦收入增长、成本降低、风险改善和服务可获得市场规模(SOM);④WSJF 优先级排序——通过 Cost of Delay / Size of Job 比值对工作排序,实现"最小投入尽早交付最大价值";延迟成本 = 业务价值 + 时间紧迫性 + 风险与机会;⑤功能级价值拆解——可按单一功能归属、均分或不均匀分配(基于触达/影响/努力等标准)。属 [[AWS-Landing-Zone]] 价值治理层的核心方法论,与 [[ctp-topic-30-managing-change]](变更管理)和 [[ctp-topic-20-program-demand-process-flow]](需求流程)共同构成完整的 CTP 治理知识体系。
|
||||||
|
|
||||||
**[[ctp-topic-20-program-demand-process-flow-and-poc-onboarding]]**(CTP Topic 20):云转型计划的程序需求流程与 POC 入职流程——Sergio 和 Damian 主讲。核心内容:①需求来源——主要由业务案例(如数据中心关闭)、高层管理人员战略优先级及产品路线图驱动;②Gate Process——Gate 0 评估准入、Gate 1 负责 Design Authority 审批、Gate 3 作为启动迁移的最终准入;③POC 目的——不仅验证架构和技术可行性,还包括让团队熟悉基于 Gruntwork 的新一代 Landing Zone;④新环境特点——强调 IaC(Terraform/Terragrunt)自动化部署,严禁手动构建;⑤PCG 团队——平台控制组,负责提供云环境支持、安全策略制定及协助产品组进行 POC;⑥成功标准——POC 成功标准必须在启动前明确定义。属 CTP 治理知识体系入口,与 [[ctp-topic-65]](价值量化)、[[ctp-topic-57]](需求管理)、[[ctp-topic-30]](变更管理)共同构成完整的治理框架链条。
|
**[[ctp-topic-20-program-demand-process-flow-and-poc-onboarding]]**(CTP Topic 20):云转型计划的程序需求流程与 POC 入职流程——Sergio 和 Damian 主讲。核心内容:①需求来源——主要由业务案例(如数据中心关闭)、高层管理人员战略优先级及产品路线图驱动;②Gate Process——Gate 0 评估准入、Gate 1 负责 Design Authority 审批、Gate 3 作为启动迁移的最终准入;③POC 目的——不仅验证架构和技术可行性,还包括让团队熟悉基于 Gruntwork 的新一代 Landing Zone;④新环境特点——强调 IaC(Terraform/Terragrunt)自动化部署,严禁手动构建;⑤PCG 团队——平台控制组,负责提供云环境支持、安全策略制定及协助产品组进行 POC;⑥成功标准——POC 成功标准必须在启动前明确定义。属 CTP 治理知识体系入口,与 [[ctp-topic-65]](价值量化)、[[ctp-topic-57]](需求管理)、[[ctp-topic-30]](变更管理)共同构成完整的治理框架链条。
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
title: "CTP Topic 3 Deploy and maintain infrastructure"
|
||||||
|
type: source
|
||||||
|
tags:
|
||||||
|
- IaC
|
||||||
|
- Deployment
|
||||||
|
- CI/CD
|
||||||
|
- CTP
|
||||||
|
- Terraform
|
||||||
|
- Terragrunt
|
||||||
|
date: 2026-04-14
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source File
|
||||||
|
- [[Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/ctp-topic-3-deploy-and-maintain-infrastructure.md]]
|
||||||
|
|
||||||
|
## Summary(用中文描述)
|
||||||
|
- 核心主题:AWS Landing Zone 环境下通过 Terraform/Terragrunt 实现基础设施部署与维护的完整方法论
|
||||||
|
- 问题域:多账户、多产品团队环境下 IaC 模块化复用、服务目录治理、Terragrunt 依赖管理
|
||||||
|
- 方法/机制:Service Module(业务视角)vs Regular Module(技术视角)的分层抽象;Terragrunt HCL 引用特定版本模块;Service Catalog 三级复用(单账户→产品团队→跨团队);Terragrunt 缓存目录预取依赖
|
||||||
|
- 结论/价值:模块化 IaC 实现独立发布周期和可维护性;Service 层抽象减少配置复杂度,越高层抽象选项越少(类 OO 继承);推荐使用专用 Service Catalog 而非相对路径引用
|
||||||
|
|
||||||
|
## Key Claims(用中文描述)
|
||||||
|
- Product Team 在 Landing Zone 中部署基础设施时,跨越多个账户(如 Artifactory 账户、AD 账户),涉及多个 Git 仓库协同
|
||||||
|
- Service Module 由 main.tf 文件引用其他仓库模块组合而成,满足特定业务需求(如 AD 服务、DNS 服务)
|
||||||
|
- Service 抽象层次高于 Regular Module,提供更少配置选项但更易使用
|
||||||
|
- Terragrunt 优于直接引用 master 分支,target 特定版本确保环境一致性
|
||||||
|
- 复用层次:单账户使用 → 产品团队 Service Catalog → Terraform Service Catalog(跨团队)
|
||||||
|
- Terragrunt 在运行前预取所有引用,通过缓存目录存储克隆的仓库
|
||||||
|
|
||||||
|
## Key Quotes
|
||||||
|
> "A service is a business requirement, while a regular module is a technical requirement." — 核心区分:Service 解决业务问题,Module 解决技术问题
|
||||||
|
|
||||||
|
> "When deploying infrastructure, Terragrunt HCL files are used to reference these services, targeting specific versions rather than the master branch." — 版本控制优于分支引用
|
||||||
|
|
||||||
|
> "The higher up the chain, the less configuration options are available, similar to an object-oriented approach." — 抽象层次与配置灵活性的反向关系
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
- [[Landing Zone(落地区)]]:云环境的基础账户结构和资源隔离框架,产品团队在此之上部署工作负载
|
||||||
|
- [[Service Module(服务模块)]]:满足业务需求的一组 Terraform 模块组合,相较于技术模块提供更高级抽象
|
||||||
|
- [[Service Catalog(服务目录)]]:可复用模块的集中管理库,支持三级复用(账户/产品团队/跨团队)
|
||||||
|
- [[Terragrunt]]:Terraform 的薄包装层,支持依赖管理、缓存和版本锁定
|
||||||
|
- [[Terraform Module]]:Terraform 的可复用配置单元,版本化管理
|
||||||
|
- [[Infrastructure as Code(IaC)]]:通过代码管理和配置基础设施的实践
|
||||||
|
|
||||||
|
## Key Entities
|
||||||
|
- [[AWS Landing Zone]]:AWS 多账户环境框架,是本文档讨论的基础部署上下文
|
||||||
|
- [[Gruntwork]]:提供 Terraform 模块参考架构的公司,本文多次引用其作为最佳实践模型
|
||||||
|
- [[Product Team]]:在 Landing Zone 中部署工作负载的业务团队,拥有独立的账户集合
|
||||||
|
|
||||||
|
## Connections
|
||||||
|
- [[ctp-topic-1-gruntwork-landing-zone-architecture]] ← foundational ← [[ctp-topic-3-deploy-and-maintain-infrastructure]]
|
||||||
|
- [[ctp-topic-9-ci-cd-with-gruntwork]] ← related ← [[ctp-topic-3-deploy-and-maintain-infrastructure]]
|
||||||
|
- [[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]] ← related ← [[ctp-topic-3-deploy-and-maintain-infrastructure]]
|
||||||
|
- [[ctp-topic-33-an-introduction-to-gitops]] ← extends ← [[ctp-topic-3-deploy-and-maintain-infrastructure]]
|
||||||
|
|
||||||
|
## Contradictions
|
||||||
|
- 与 [[ctp-topic-39-implementing-eks-in-the-aws-lab-landing-zone]] 的 Atlantis 部分存在约束差异:
|
||||||
|
- 冲突点:Atlantis 对 EKS 部署的支持能力
|
||||||
|
- 当前观点(Topic 3):Terragrunt 可用于所有基础设施部署,包括 EKS
|
||||||
|
- 对方观点(Topic 39):Atlantis 当前不支持 EKS 部署,需通过 Jenkins + Terragrunt 模块替代
|
||||||
|
- 评估:Topic 39 提供更具体的实践经验,Topic 3 提供通用原则,两者约束条件不同不构成直接矛盾
|
||||||
62
wiki/sources/ctp-topic-33-an-introduction-to-gitops.md
Normal file
62
wiki/sources/ctp-topic-33-an-introduction-to-gitops.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
title: "CTP Topic 33 An Introduction to GitOps"
|
||||||
|
type: source
|
||||||
|
tags:
|
||||||
|
- GitOps
|
||||||
|
- CI/CD
|
||||||
|
- Kubernetes
|
||||||
|
- DevOps
|
||||||
|
- Infrastructure as Code
|
||||||
|
date: 2026-04-14
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source File
|
||||||
|
- [[Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/ctp-topic-33-an-introduction-to-gitops.md]]
|
||||||
|
|
||||||
|
## Summary(用中文描述)
|
||||||
|
- 核心主题:GitOps 方法论入门——将软件开发原则应用于部署流程,实现声明式基础设施自动化交付
|
||||||
|
- 问题域:解决部署失败、配置不一致、手动操作风险等传统 CI/CD 问题
|
||||||
|
- 方法/机制:四大原则(声明式配置 + 版本控制 + CD 流程分离 + 自修复协调器)+ Pull/Push 两种部署模型
|
||||||
|
- 结论/价值:开发者只需掌握 Git 即可完成安全部署,代码变更分钟级上线,Git 日志即审计追踪
|
||||||
|
|
||||||
|
## Key Claims(用中文描述)
|
||||||
|
- GitOps 四大原则使部署过程完全自动化,代码变更可在数分钟内安全部署上线
|
||||||
|
- Pull 模型比 Push 模型更适合 GitOps——部署代理同时监控 Git 和目标系统,提供额外安全层
|
||||||
|
- 幂等(Idempotent)平台(如 Kubernetes)是 CD 流程顺利执行的必要条件
|
||||||
|
- GitOps 是 DevOps 的逻辑演进,Git 提交日志天然构成合规审计追踪
|
||||||
|
- CI 与 CD 应解耦——CI 专注构建和分析代码,CD 专注部署二进制文件,解耦增强安全性
|
||||||
|
|
||||||
|
## Key Quotes
|
||||||
|
> "The only tool a developer needs to know is Git." — Victor Etkin
|
||||||
|
> "GitOps uses Git workflows, CD pipelines, and infrastructure as code. Observability is crucial for ensuring the desired and actual states align."
|
||||||
|
> "An IDEMPOTENT operation is one that can be applied multiple times without changing the result beyond the initial application."
|
||||||
|
> "GitOps is a logical evolution of DevOps, simplifying adoption and enhancing portability. Git commit logs become audit trails, streamlining compliance."
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
- [[GitOps]]:将软件开发原则(Git 版本控制 + Pull Request 协作)应用于基础设施和应用部署的方法论,核心是通过声明式配置描述期望状态,GitOps 控制器自动协调实际状态向期望状态收敛
|
||||||
|
- [[Idempotent Deployment(幂等部署)]]:同一操作可重复执行而结果不变的特性,是 GitOps CD 流程顺利运行的必要前提,Kubernetes 是典型的幂等平台
|
||||||
|
- [[Pull Model]]:GitOps 推荐部署模型——部署代理持续监控 Git 仓库和目标系统状态,检测到差异时自动从 Git 拉取变更并应用,天然提供额外安全层(系统状态不暴露给外部)
|
||||||
|
- [[Push Model]]:CI/CD 管道主动推送变更到目标系统的部署模式,相比 Pull 模型安全性较低但实现更简单
|
||||||
|
- [[Declarative Configuration(声明式配置)]]:通过代码描述"系统应该是什么状态"而非"如何一步步到达该状态",是 GitOps 和 Infrastructure as Code 的核心原则
|
||||||
|
- [[Infrastructure as Code(基础设施即代码)]]:用代码管理基础设施的实践,与 GitOps 高度协同,共同构成自动化部署的基础
|
||||||
|
- [[GitOps Controller]]:运行在目标环境中的自动化代理,持续比对 Git 中声明的期望状态与系统实际状态,自动调和偏差,无需人工干预
|
||||||
|
|
||||||
|
## Key Entities
|
||||||
|
- [[Victor Etkin]]:GitOps 入门视频主讲人,阐述 GitOps 四大原则及 Pull 模型优势
|
||||||
|
- [[Weaveworks]]:GitOps 概念的提出者和早期推广者(视频背景知识)
|
||||||
|
- [[Kubernetes]]:GitOps 最常用的部署目标平台,其声明式 API 和自修复机制与 GitOps 高度契合
|
||||||
|
- [[Atlantis]]:基于 Pull Request 的 Terraform IaC 自动化工具(参见 [[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]]),属 GitOps 工具实践层
|
||||||
|
|
||||||
|
## Connections
|
||||||
|
- [[ctp-topic-2-git]] ← foundational_skill ← [[GitOps]](Git 版本控制是 GitOps 的基础工具)
|
||||||
|
- [[ctp-topic-9-ci-cd-with-gruntwork]] ← extends ← [[GitOps]](CI/CD 是 GitOps 的核心组件)
|
||||||
|
- [[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]] ← implements ← [[GitOps]](Atlantis 是 GitOps 工具实践)
|
||||||
|
- [[GitOps]] ← complements ← [[DevOps]](GitOps 是 DevOps 的逻辑演进)
|
||||||
|
- [[Amazon EKS]] ← platform ← [[GitOps]](K8s 是 GitOps 最常用目标平台)
|
||||||
|
- [[GitOps]] ← extends ← [[Infrastructure as Code]](GitOps 是 IaC 的部署编排层)
|
||||||
|
|
||||||
|
## Contradictions
|
||||||
|
- 与 [[ctp-topic-39-implementing-eks-in-the-aws-lab-landing-zone]] 存在实践约束差异:
|
||||||
|
- 冲突点:Atlantis 当前不支持 EKS 部署
|
||||||
|
- 当前观点(Topic 33):Kubernetes 是 GitOps 的主要应用场景
|
||||||
|
- 对方观点(Topic 39):Atlantis 需通过 Jenkins + Terragrunt 替代方案处理 EKS 工作负载
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
title: "Public Cloud Learning Sessions - Ollie Workflow and The Demand Process - 20240416"
|
||||||
|
type: source
|
||||||
|
tags:
|
||||||
|
- Workflow
|
||||||
|
- Demand-Process
|
||||||
|
- FinOps
|
||||||
|
- ITIL
|
||||||
|
- SMACs
|
||||||
|
date: 2024-04-16
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source File
|
||||||
|
- [[Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/public-cloud-learning-sessions-ollie-workflow-and-the-demand-process-20240416-16.md]]
|
||||||
|
|
||||||
|
## Summary(用中文描述)
|
||||||
|
- 核心主题:Oli 工作流(超大规模云厂商支出审批流程)与需求管理全链路
|
||||||
|
- 问题域:云转型过程中的 FinOps 支出审批治理、需求提交与自动化履约
|
||||||
|
- 方法/机制:ITIL 服务管理框架下的三级审批工作流(FinOps 可行性→云服务技术可行性→FPNA 预算可用性),以及 OpenText 端到端需求管理流程(Octane/Qixi 提交 → 主服务目录 → SMACs 嵌入 → 自动化履约)
|
||||||
|
- 结论/价值:所有超大规模云厂商支出(含工程实验室和商业工作负载)无论金额均需 MUI/Shannon 书面审批;推动"机器做机器能做的事",目标是 80% 场景业务单元自助完成需求提交
|
||||||
|
|
||||||
|
## Key Claims(用中文描述)
|
||||||
|
- 所有超大规模云厂商支出(含工程实验室和商业工作负载空间)无论金额,均需 MUI 或 Shannon 书面审批
|
||||||
|
- Oli 工作流由 Tom Bice 领导的 FinOps 团队接管,正在集成到 SMACs 平台
|
||||||
|
- 提议的三阶段工作流:FinOps 可行性验证 → 云服务技术可行性验证 → FPNA 团队预算可用性验证
|
||||||
|
- Oli 系统提供飞行中 CSV 报告,追踪工作流状态、申请人、成本中心、月成本及当前步骤
|
||||||
|
- ITIL 框架将业务流程分为服务战略、设计、过渡、运营、持续改进五个阶段
|
||||||
|
- 主服务目录(Combined Cloud Products Master Catalog)将嵌入 SMACs,目标是 80% 场景下业务单元可自助选择所需服务
|
||||||
|
- ADM 和 ITOM 需求规划会议记录所需内容、数量和发布版本
|
||||||
|
|
||||||
|
## Key Quotes
|
||||||
|
> "If justification details are not provided, requests are subject to immediate rejection." — Oli 请求提交规范
|
||||||
|
> "Machines should do what machines can do, enabling an automated fulfillment process." — OpenText 需求管理核心理念
|
||||||
|
> "The goal is for business units to self-select what they need 80% of the time." — 需求管理自动化目标
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
- [[Demand-Management(需求管理)]]:平衡需求与可用容量的必要手段,是 ITIL 服务过渡阶段的关键活动
|
||||||
|
- [[ITIL-Service-Management]]:将业务流程分为服务战略、服务设计、服务过渡、服务运营、持续服务改进五阶段,Oli 工作流对应请求履约的第一阶段
|
||||||
|
- [[SMACs]]:Social、Mobile、Analytics、Cloud 的技术栈组合,Oli 工作流正在集成到 SMACs 平台
|
||||||
|
- [[FinOps]]:财务运营,Tom Bice 团队负责 Oli 工作流接管,重点关注云支出的可视性与优化
|
||||||
|
- [[Product-Backlog]]:产品待办列表,Oli 工作流产生的请求经审批后进入 Backlog 管理
|
||||||
|
|
||||||
|
## Key Entities
|
||||||
|
- [[Tom-Bice]]:FinOps 团队负责人,正在接管 Oli 工作流并集成到 SMACs
|
||||||
|
- [[FPNA-Team]]:财务规划与分析团队,负责工作流第三阶段——预算可用性验证
|
||||||
|
- [[MUI]]:超大规模云厂商支出审批人之一(与 Shannon 共同审批所有云支出请求)
|
||||||
|
- [[Shannon]]:超大规模云厂商支出审批人之一(与 MUI 共同审批所有云支出请求)
|
||||||
|
- [[Octane]]:超大规模云厂商 SaaS 产品需求管理平台,业务单元可直接向其提交需求
|
||||||
|
- [[Qixi]]:Oli 需求提交流程的前端接口之一,业务单元通过其提交需求
|
||||||
|
|
||||||
|
## Connections
|
||||||
|
- [[ctp-topic-57-product-backlog-managing-demand]] ← extends ← 本文档(Oli 工作流审批通过的请求进入产品 Backlog 管理管道)
|
||||||
|
- [[ctp-topic-65-tracing-the-value-delivered-in-cloud-transformation]] ← depends_on ← 本文档(需求管理是价值交付量化框架的前置管道)
|
||||||
|
- [[public-cloud-learning-sessions-applicable-business-analysis-techniques-20240109]] ← related_to ← 本文档(BOSCARD 框架是需求分析的前置技法)
|
||||||
|
- [[ctp-topic-4-using-agile-to-run-the-cloud-transformation-program]] ← related_to ← 本文档(Kanban 敏捷实践为需求流转提供方法论支撑)
|
||||||
|
|
||||||
|
## Contradictions
|
||||||
|
- 无已知冲突页面
|
||||||
Reference in New Issue
Block a user