> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked. # Fonrey — 组织人事数据模型(DATA_MODEL_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 | | 软删除时间戳;NULL=未删除,非NULL=已软删除 | **关键索引**: ```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() | 记录创建时间(系统自动,等同 operated_at) | | ⚠️ 无 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 | | 软删除时间戳;NULL=未删除,非NULL=已软删除 | --- ### 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`