Files
nexus/Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md
2026-04-25 07:44:21 +08:00

15 KiB
Raw Blame History

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 软删除

关键索引

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 软删除(离职员工仍保留记录)

关键索引

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