15 KiB
15 KiB
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安居客 / 中国网络经纪人等。
关键业务规则
- 组织层级约束:店组级部门 必须 挂在门店下;经纪人/店管的所属部门 只能 是门店或店组。
- 经纪人定义:职务类别为「置业顾问」的员工即为经纪人,受业务规则特殊约束。
- 人员异动强制日志:入职、调动、离职、复职等操作均自动生成
staff_transfer_logs记录,不可删除。 - 账号与员工联动:员工离职后,对应的
auth_user.is_active设为False,不可登录;复职后由管理员手动恢复。 - 手机号敏感字段:员工手机号 AES-256-GCM 加密存储,SHA-256 哈希用于唯一性校验,通讯录展示脱敏格式。
- 数据归属继承:员工调动时,名下房源/客源默认跟随员工到新部门;离职时可选择转移给指定账号。
二、实体关系
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 | 软删除 |
关键索引:
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 | 通讯录是否隐藏手机号 |
| 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 | 软删除(离职员工仍保留记录) |
关键索引:
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 = 账号恢复
关键索引:
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 查询某部门及所有下级的在职员工
-- 利用物化路径高效查询子树
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 查询员工完整异动历史
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 获取员工的直接上下级链
-- 直属上级
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