# 设计文档:ETL 员工维度表(staff_info) ## 概述 为飞球 ETL 连接器新增员工维度表,从 `SearchSystemStaffInfo` API 抓取球房全体员工数据(店长、主管、教练、收银员、助教管理员等),经 ODS 落地后清洗装载至 DWD 层。员工表与现有助教表(`assistant_accounts_master`)是完全独立的实体。 ## API 响应结构 ```json { "data": { "total": 15, "staffProfiles": [ { "id": 3020236636900101, "cashierPointId": 2790685415443270, "cashierPointName": "默认", "job_num": "", "staff_name": "葛芃", "mobile": "13811638071", "auth_code": "", "avatar": "", "create_time": "2025-12-24 00:03:37", "entry_time": "2025-12-23 08:00:00", "is_delete": 0, "leave_status": 0, "resign_time": "2225-12-24 00:03:37", "site_id": 2790685415443269, "staff_identity": 2, "status": 1, "system_role_id": 4, "system_user_id": 3020236636293893, "tenant_id": 2790683160709957, "tenant_org_id": 2790685415443269, "job": "店长", "shop_name": "朗朗桌球", "account_status": 1, "is_reserve": 1, "groupName": "", "groupId": 0, "alias_name": "葛芃", "staff_profile_id": 0, "site_label": "", "rank_id": -1, "ding_talk_synced": 1, "new_rank_id": 0, "new_staff_identity": 0, "salary_grant_enabled": 2, "rankName": "无职级", "entry_type": 1, "userRoles": [], "entry_sign_status": 0, "resign_sign_status": 0, "criticism_status": 1, "gender": 3 } ] }, "code": 0 } ``` ## 1. ODS 层设计 ### 1.1 ODS 任务规格 ```python OdsTaskSpec( code="ODS_STAFF_INFO", class_name="OdsStaffInfoTask", table_name="ods.staff_info_master", endpoint="/PersonnelManagement/SearchSystemStaffInfo", data_path=("data",), list_key="staffProfiles", pk_columns=(_int_col("id", "id", required=True),), extra_params={ "workStatusEnum": 0, "dingTalkSynced": 0, "staffIdentity": 0, "rankId": 0, "criticismStatus": 0, "signStatus": -1, }, include_source_endpoint=False, include_fetched_at=False, include_record_index=True, requires_window=False, time_fields=None, snapshot_mode=SnapshotMode.FULL_TABLE, description="员工档案 ODS:SearchSystemStaffInfo -> staffProfiles 原始 JSON", ) ``` ### 1.2 ODS 表 DDL:`ods.staff_info_master` ```sql CREATE TABLE ods.staff_info_master ( id BIGINT NOT NULL, tenant_id BIGINT, site_id BIGINT, tenant_org_id BIGINT, system_user_id BIGINT, staff_name TEXT, alias_name TEXT, mobile TEXT, avatar TEXT, gender INTEGER, job TEXT, job_num TEXT, staff_identity INTEGER, status INTEGER, account_status INTEGER, system_role_id INTEGER, rank_id INTEGER, rank_name TEXT, new_rank_id INTEGER, new_staff_identity INTEGER, leave_status INTEGER, entry_time TIMESTAMP WITHOUT TIME ZONE, resign_time TIMESTAMP WITHOUT TIME ZONE, create_time TIMESTAMP WITHOUT TIME ZONE, is_delete INTEGER, is_reserve INTEGER, shop_name TEXT, site_label TEXT, cashier_point_id BIGINT, cashier_point_name TEXT, group_id BIGINT, group_name TEXT, staff_profile_id BIGINT, auth_code TEXT, auth_code_create TIMESTAMP WITHOUT TIME ZONE, ding_talk_synced INTEGER, salary_grant_enabled INTEGER, entry_type INTEGER, entry_sign_status INTEGER, resign_sign_status INTEGER, criticism_status INTEGER, user_roles JSONB, -- ETL 元数据 content_hash TEXT NOT NULL, source_file TEXT, fetched_at TIMESTAMP WITH TIME ZONE DEFAULT now(), payload JSONB NOT NULL ); COMMENT ON TABLE ods.staff_info_master IS '员工档案主数据(来源:SearchSystemStaffInfo API)'; ``` ### 1.3 ODS 列名映射说明 API 返回的驼峰字段在 ODS 层统一转为蛇形命名(由 BaseOdsTask 自动处理): - `cashierPointId` → `cashier_point_id` - `cashierPointName` → `cashier_point_name` - `staffName` / `staff_name` → `staff_name`(API 已是蛇形) - `systemUserId` / `system_user_id` → `system_user_id` - `tenantOrgId` / `tenant_org_id` → `tenant_org_id` - `groupName` → `group_name`(注意:API 返回驼峰 `groupName`) - `groupId` → `group_id`(API 返回驼峰 `groupId`) - `rankName` → `rank_name`(API 返回驼峰 `rankName`) - `userRoles` → `user_roles`(数组,存为 JSONB) - `authCodeCreate` / `auth_code_create` → `auth_code_create` ## 2. DWD 层设计 ### 2.1 主表 DDL:`dwd.dim_staff` 核心业务字段,高频查询使用。 ```sql CREATE TABLE dwd.dim_staff ( staff_id BIGINT NOT NULL, staff_name TEXT, alias_name TEXT, mobile TEXT, gender INTEGER, job TEXT, tenant_id BIGINT, site_id BIGINT, system_role_id INTEGER, staff_identity INTEGER, status INTEGER, leave_status INTEGER, entry_time TIMESTAMP WITH TIME ZONE, resign_time TIMESTAMP WITH TIME ZONE, is_delete INTEGER, -- SCD2 scd2_start_time TIMESTAMP WITH TIME ZONE NOT NULL, scd2_end_time TIMESTAMP WITH TIME ZONE, scd2_is_current INTEGER, scd2_version INTEGER, PRIMARY KEY (staff_id, scd2_start_time) ); COMMENT ON TABLE dwd.dim_staff IS '员工档案维度主表(SCD2)'; ``` ### 2.2 扩展表 DDL:`dwd.dim_staff_ex` 次要/低频变更字段。 ```sql CREATE TABLE dwd.dim_staff_ex ( staff_id BIGINT NOT NULL, avatar TEXT, job_num TEXT, account_status INTEGER, rank_id INTEGER, rank_name TEXT, new_rank_id INTEGER, new_staff_identity INTEGER, is_reserve INTEGER, shop_name TEXT, site_label TEXT, tenant_org_id BIGINT, system_user_id BIGINT, cashier_point_id BIGINT, cashier_point_name TEXT, group_id BIGINT, group_name TEXT, staff_profile_id BIGINT, auth_code TEXT, auth_code_create TIMESTAMP WITH TIME ZONE, ding_talk_synced INTEGER, salary_grant_enabled INTEGER, entry_type INTEGER, entry_sign_status INTEGER, resign_sign_status INTEGER, criticism_status INTEGER, create_time TIMESTAMP WITH TIME ZONE, user_roles JSONB, -- SCD2 scd2_start_time TIMESTAMP WITH TIME ZONE NOT NULL, scd2_end_time TIMESTAMP WITH TIME ZONE, scd2_is_current INTEGER, scd2_version INTEGER, PRIMARY KEY (staff_id, scd2_start_time) ); COMMENT ON TABLE dwd.dim_staff_ex IS '员工档案维度扩展表(SCD2)'; ``` ### 2.3 TABLE_MAP 映射 ```python # 在 DwdLoadTask.TABLE_MAP 中新增: "dwd.dim_staff": "ods.staff_info_master", "dwd.dim_staff_ex": "ods.staff_info_master", ``` ### 2.4 FACT_MAPPINGS 字段映射 ```python # dim_staff 主表映射 "dwd.dim_staff": [ ("staff_id", "id", None), ("entry_time", "entry_time", "timestamptz"), ("resign_time", "resign_time", "timestamptz"), ], # dim_staff_ex 扩展表映射 "dwd.dim_staff_ex": [ ("staff_id", "id", None), ("rank_name", "rankname", None), ("cashier_point_id", "cashierpointid", "bigint"), ("cashier_point_name", "cashierpointname", None), ("group_id", "groupid", "bigint"), ("group_name", "groupname", None), ("system_user_id", "systemuserid", "bigint"), ("tenant_org_id", "tenantorgid", "bigint"), ("auth_code_create", "auth_code_create", "timestamptz"), ("create_time", "create_time", "timestamptz"), ("user_roles", "userroles", "jsonb"), ], ``` 说明: - ODS 层的列名由 BaseOdsTask 自动从 API 驼峰转为蛇形(如 `cashierPointId` → `cashierpointid`,注意 PG 列名全小写无下划线) - DWD 主表中 `staff_name`、`alias_name`、`mobile` 等与 ODS 同名列自动映射,无需显式配置 - `staff_id` 映射自 ODS 的 `id` 列 ## 3. 数据流概览 ``` API: SearchSystemStaffInfo ↓ (POST, 分页, extra_params 筛选) ODS: ods.staff_info_master ↓ (SCD2 合并, FULL_TABLE 快照) DWD: dwd.dim_staff + dwd.dim_staff_ex ``` ## 4. 测试框架 - 测试框架:`pytest` + `hypothesis` - 单元测试使用 `FakeDB` / `FakeAPI`(`tests/unit/task_test_utils.py`) ## 5. 正确性属性 ### P1:ODS 任务规格完整性 对于 `ODS_STAFF_INFO` 任务规格,以下属性必须成立: - `code == "ODS_STAFF_INFO"` - `table_name == "ods.staff_info_master"` - `endpoint == "/PersonnelManagement/SearchSystemStaffInfo"` - `list_key == "staffProfiles"` - `snapshot_mode == SnapshotMode.FULL_TABLE` - `requires_window == False` - `time_fields is None` - `"staffProfiles"` 存在于 `DEFAULT_LIST_KEYS` 中 - `"ODS_STAFF_INFO"` 存在于 `ENABLED_ODS_CODES` 中 验证方式:单元测试直接断言 ### P2:DWD 映射完整性 对于 DWD 装载配置,以下属性必须成立: - `TABLE_MAP["dwd.dim_staff"] == "ods.staff_info_master"` - `TABLE_MAP["dwd.dim_staff_ex"] == "ods.staff_info_master"` - `FACT_MAPPINGS["dwd.dim_staff"]` 包含 `staff_id` → `id` 的映射 - `FACT_MAPPINGS["dwd.dim_staff_ex"]` 包含 `staff_id` → `id` 的映射 验证方式:单元测试直接断言 ### P3:ODS 列名提取一致性(属性测试) 对于任意 API 返回的员工记录(含驼峰和蛇形混合字段名),经 BaseOdsTask 处理后: - 所有字段名转为小写蛇形 - `id` 字段不为空且为正整数 - `payload` 字段包含完整原始 JSON 验证方式:hypothesis 属性测试,生成随机员工记录验证转换一致性 ## 6. 文件变更清单 ### 代码变更 | 文件 | 变更类型 | 说明 | |------|----------|------| | `apps/etl/connectors/feiqiu/api/client.py` | 修改 | `DEFAULT_LIST_KEYS` 添加 `"staffProfiles"` | | `apps/etl/connectors/feiqiu/tasks/ods/ods_tasks.py` | 修改 | 新增 `ODS_STAFF_INFO` 任务规格 + 注册到 `ENABLED_ODS_CODES` | | `apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py` | 修改 | `TABLE_MAP` 和 `FACT_MAPPINGS` 新增 dim_staff/dim_staff_ex 映射 | ### DDL / 迁移 | 文件 | 变更类型 | 说明 | |------|----------|------| | `db/etl_feiqiu/migrations/2026-02-22__add_staff_info_tables.sql` | 新增 | ODS + DWD 建表迁移脚本 | | `docs/database/ddl/etl_feiqiu__ods.sql` | 修改 | 追加 `ods.staff_info_master` DDL | | `docs/database/ddl/etl_feiqiu__dwd.sql` | 修改 | 追加 `dwd.dim_staff` + `dwd.dim_staff_ex` DDL | ### 文档 | 文件 | 变更类型 | 说明 | |------|----------|------| | `apps/etl/connectors/feiqiu/docs/database/ODS/mappings/mapping_SearchSystemStaffInfo_staff_info_master.md` | 新增 | API→ODS 字段映射文档 | | `apps/etl/connectors/feiqiu/docs/database/ODS/main/BD_manual_staff_info_master.md` | 新增 | ODS 表 BD 手册 | | `apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff.md` | 新增 | DWD 主表 BD 手册 | | `apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff_ex.md` | 新增 | DWD 扩展表 BD 手册 | | `apps/etl/connectors/feiqiu/docs/database/README.md` | 修改 | 增加员工表条目 | | `apps/etl/connectors/feiqiu/docs/etl_tasks/ods_tasks.md` | 修改 | 增加 ODS_STAFF_INFO 任务说明 | | `docs/database/README.md` | 修改 | 增加员工相关表条目 |