# 人事板块 - 员工档案对接真实数据 ## 实施时间 2025-11-19 01:50 --- ## 📋 **一、需求概述** 将人事板块的员工档案列表从硬编码假数据切换到 Profile 表的真实数据,并在 `Profile.data.hrData` 中添加 HR 专用字段。 --- ## 🗄️ **二、数据表结构** ### Profile 表(员工信息表) #### 直接字段 ```typescript Profile { id: string; // 员工ID name: string; // 昵称(企微昵称) mobile: string; // 手机号 email: string; // 邮箱 roleName: string; // 角色名称 department: Pointer; // 所属部门 isDeleted: boolean; // 是否删除 isDisabled: boolean; // 是否禁用(离职) createdAt: Date; // 创建时间 } ``` #### data 字段(JSON) ```typescript Profile.data = { // 基础信息 realname: string; // 真实姓名 gender: string; // 性别 mobile: string; // 手机号(备用) email: string; // 邮箱(备用) position: string; // 职位 // 企微信息 wxworkInfo: { name: string; // 企微昵称 mobile: string; // 企微手机号 email: string; // 企微邮箱 position: string; // 企微职位 gender: string; // 企微性别 avatar: string; // 企微头像 userid: string; // 企微用户ID }, // HR 专用字段(新增)✅ hrData: { employeeId: string; // 工号 idCard: string; // 身份证号 bankCard: string; // 银行卡号 birthDate: Date | string; // 出生日期 hireDate: Date | string; // 入职日期 gender: string; // 性别 position: string; // 职位 employmentStatus: string; // 就业状态 // - 'active': 在职 // - 'probation': 试用期 // - 'inactive': 停薪留职 } } ``` --- ## 🔄 **三、字段映射关系** ### 员工档案显示字段 → Profile 表字段 | 档案字段 | Profile 字段 | 优先级 | 说明 | |---------|-------------|--------|------| | 姓名 | `data.realname` → `wxworkInfo.name` → `name` | 优先真实姓名 | 显示真实姓名 | | 工号 | `data.hrData.employeeId` | 唯一来源 | HR 专用字段 | | 部门 | `department.name` | 唯一来源 | 关联 Department 表 | | 职位 | `wxworkInfo.position` → `data.position` → `hrData.position` → `roleName` | 多来源 | 企微职位优先 | | 手机号 | `wxworkInfo.mobile` → `data.mobile` → `mobile` | 多来源 | 企微手机号优先 | | 邮箱 | `wxworkInfo.email` → `data.email` → `email` | 多来源 | 企微邮箱优先 | | 性别 | `wxworkInfo.gender` → `data.gender` → `hrData.gender` | 多来源 | 企微性别优先 | | 出生日期 | `data.hrData.birthDate` | 唯一来源 | HR 专用字段 | | 入职日期 | `data.hrData.hireDate` → `createdAt` | 两来源 | 无 hrData 时用创建时间 | | 状态 | 根据 `isDisabled` + `hrData.employmentStatus` 计算 | 计算字段 | 见状态映射规则 | | 身份证号 | `data.hrData.idCard` | 唯一来源 | HR 专用字段,脱敏显示 | | 银行卡号 | `data.hrData.bankCard` | 唯一来源 | HR 专用字段,脱敏显示 | --- ### 员工状态映射规则 ```typescript if (profile.isDisabled) { status = '离职'; } else if (hrData.employmentStatus === 'probation') { status = '试用期'; } else if (hrData.employmentStatus === 'inactive') { status = '停薪留职'; } else { status = '在职'; // 默认 } ``` --- ## ✅ **四、主要修改内容** ### 1. 引入 Parse 依赖 **文件**:`employee-records.ts` ```typescript import { FmodeParse, FmodeObject } from 'fmode-ng/parse'; const Parse = FmodeParse.with('nova'); ``` --- ### 2. 修改数据源 **原代码**(硬编码假数据): ```typescript employees = signal([ { id: '1', name: '张三', department: '设计部', // ... 硬编码数据 } ]); ``` **新代码**(从 Profile 表加载): ```typescript // 员工数据 - 从 Profile 表加载 employees = signal([]); // 加载状态 loading = signal(false); loadError = signal(''); ``` --- ### 3. 新增数据加载方法 **文件**:`employee-records.ts` ```typescript /** * 从 Profile 表加载真实的员工数据 */ async loadEmployees(): Promise { this.loading.set(true); this.loadError.set(''); try { console.log('📋 [员工档案] 开始加载员工数据...'); // 查询所有 Profile 记录 const query = new Parse.Query('Profile'); query.notEqualTo('isDeleted', true); query.include('department'); query.ascending('name'); query.limit(1000); const profiles = await query.find(); console.log(`✅ [员工档案] 查询到 ${profiles.length} 个员工`); // 转换为 Employee 格式 const employeeList: Employee[] = profiles.map(profile => { const data = profile.get('data') || {}; const wxworkInfo = data.wxworkInfo || {}; const hrData = data.hrData || {}; // HR 相关数据 // 获取部门名称 const department = profile.get('department'); const departmentName = department?.get ? department.get('name') : ''; // 手机号优先级:企微 > data.mobile > mobile 字段 const phone = wxworkInfo.mobile || data.mobile || profile.get('mobile') || ''; // 员工状态映射 let status: EmployeeStatus = '在职'; if (profile.get('isDisabled')) { status = '离职'; } else if (hrData.employmentStatus === 'probation') { status = '试用期'; } else if (hrData.employmentStatus === 'inactive') { status = '停薪留职'; } return { id: profile.id, name: data.realname || wxworkInfo.name || profile.get('name') || '未知', department: departmentName || '未分配', position: wxworkInfo.position || data.position || hrData.position || profile.get('roleName') || '', employeeId: hrData.employeeId || '', // 工号 phone: phone, email: wxworkInfo.email || data.email || profile.get('email') || '', gender: wxworkInfo.gender || data.gender || hrData.gender || '', birthDate: hrData.birthDate ? new Date(hrData.birthDate) : undefined, hireDate: hrData.hireDate ? new Date(hrData.hireDate) : (profile.get('createdAt') || new Date()), status: status, idCard: hrData.idCard || '', // 身份证号 bankCard: hrData.bankCard || '' // 银行卡号 }; }); this.employees.set(employeeList); console.log(`✅ [员工档案] 员工数据加载完成,共 ${employeeList.length} 个`); } catch (error) { console.error('❌ [员工档案] 加载员工数据失败:', error); this.loadError.set('加载员工数据失败,请刷新重试'); this.showSnackBar('加载员工数据失败,请刷新重试', 'error'); } finally { this.loading.set(false); } } ``` --- ### 4. 新增 HR 数据保存方法 **文件**:`employee-records.ts` ```typescript /** * 保存员工 HR 数据到 Profile.data.hrData */ async saveEmployeeHRData(profileId: string, hrData: any): Promise { try { const query = new Parse.Query('Profile'); const profile = await query.get(profileId); const currentData = profile.get('data') || {}; profile.set('data', { ...currentData, hrData: { ...currentData.hrData, ...hrData } }); await profile.save(); console.log(`✅ [员工档案] 保存员工 HR 数据成功: ${profileId}`); } catch (error) { console.error('❌ [员工档案] 保存员工 HR 数据失败:', error); throw error; } } ``` --- ### 5. 修改新增员工逻辑 **原代码**(更新本地数组): ```typescript dialogRef.afterClosed().subscribe(result => { if (result) { const newEmployee: Employee = { id: (this.employees().length + 1).toString(), ...result }; this.employees.update(employees => [...employees, newEmployee]); this.showSnackBar('员工添加成功'); } }); ``` **新代码**(保存到 Profile 表): ```typescript dialogRef.afterClosed().subscribe(async (result) => { if (result) { try { // 创建新的 Profile 记录 const ProfileClass = Parse.Object.extend('Profile'); const newProfile = new ProfileClass(); newProfile.set('name', result.name); newProfile.set('mobile', result.phone); newProfile.set('email', result.email); newProfile.set('roleName', result.position); // 设置部门(如果有) if (result.department) { const deptQuery = new Parse.Query('Department'); deptQuery.equalTo('name', result.department); const dept = await deptQuery.first(); if (dept) { newProfile.set('department', dept); } } // 设置 HR 数据 newProfile.set('data', { realname: result.name, gender: result.gender, email: result.email, mobile: result.phone, position: result.position, hrData: { employeeId: result.employeeId, idCard: result.idCard, bankCard: result.bankCard, birthDate: result.birthDate, hireDate: result.hireDate, gender: result.gender, position: result.position, employmentStatus: 'active' } }); await newProfile.save(); // 重新加载员工列表 await this.loadEmployees(); this.showSnackBar('员工添加成功'); } catch (error) { console.error('❌ [员工档案] 添加员工失败:', error); this.showSnackBar('添加员工失败,请重试', 'error'); } } }); ``` --- ### 6. 修改编辑员工逻辑 类似的修改,将本地数组更新改为 Profile 表更新: ```typescript dialogRef.afterClosed().subscribe(async (result) => { if (result) { try { // 更新 Profile 记录 const query = new Parse.Query('Profile'); const profile = await query.get(employee.id); profile.set('name', result.name); profile.set('mobile', result.phone); profile.set('email', result.email); profile.set('roleName', result.position); // 更新部门 if (result.department) { const deptQuery = new Parse.Query('Department'); deptQuery.equalTo('name', result.department); const dept = await deptQuery.first(); if (dept) { profile.set('department', dept); } } // 更新 data 字段 const currentData = profile.get('data') || {}; profile.set('data', { ...currentData, realname: result.name, gender: result.gender, email: result.email, mobile: result.phone, position: result.position, hrData: { ...currentData.hrData, employeeId: result.employeeId, idCard: result.idCard, bankCard: result.bankCard, birthDate: result.birthDate, hireDate: result.hireDate, gender: result.gender, position: result.position } }); await profile.save(); // 重新加载员工列表 await this.loadEmployees(); this.showSnackBar('员工信息更新成功'); } catch (error) { console.error('❌ [员工档案] 更新员工失败:', error); this.showSnackBar('更新员工失败,请重试', 'error'); } } }); ``` --- ### 7. 在 ngOnInit 中加载数据 ```typescript ngOnInit() { this.loadEmployees(); } ``` --- ### 8. 优化错误提示 ```typescript showSnackBar(message: string, type: 'success' | 'error' = 'success') { this.snackBar.open(message, '关闭', { duration: 3000, horizontalPosition: 'center', verticalPosition: 'top', panelClass: type === 'error' ? 'hr-snackbar-error' : 'hr-snackbar' }); } ``` --- ## 🎯 **五、新增的 HR 字段** ### Profile.data.hrData 结构 ```typescript { employeeId: string; // 工号(如:DS001, CS002) idCard: string; // 身份证号(18位) bankCard: string; // 银行卡号(16-19位) birthDate: Date | string; // 出生日期(ISO 格式) hireDate: Date | string; // 入职日期(ISO 格式) gender: string; // 性别('男' 或 '女') position: string; // 职位名称 employmentStatus: string; // 就业状态 // - 'active': 在职 // - 'probation': 试用期 // - 'inactive': 停薪留职 } ``` --- ### 字段用途说明 #### 1. **工号 (employeeId)** ``` 用途:唯一标识员工,用于考勤、薪资等 格式:部门缩写 + 序号(如:DS001) 示例: - DS001: 设计部001号 - CS002: 客服部002号 - FE001: 前端开发001号 ``` #### 2. **身份证号 (idCard)** ``` 用途:员工身份验证,用于社保、公积金等 格式:18位身份证号 安全:界面脱敏显示(前6位+****+后4位) 示例:110105199001011234 显示:110105********1234 ``` #### 3. **银行卡号 (bankCard)** ``` 用途:工资发放 格式:16-19位银行卡号 安全:界面脱敏显示(前4位+****+后4位) 示例:6222021234567890 显示:6222 **** **** 7890 ``` #### 4. **出生日期 (birthDate)** ``` 用途:员工年龄计算、生日提醒 格式:Date 对象或 ISO 字符串 示例:new Date('1990-01-01') ``` #### 5. **入职日期 (hireDate)** ``` 用途:工龄计算、试用期管理 格式:Date 对象或 ISO 字符串 示例:new Date('2022-01-15') 备注:无此字段时使用 Profile.createdAt ``` #### 6. **就业状态 (employmentStatus)** ``` 值: - 'active': 在职(正式员工) - 'probation': 试用期 - 'inactive': 停薪留职 - 无值:默认为在职 影响: - isDisabled = true → 状态显示为"离职" - employmentStatus = 'probation' → 状态显示为"试用期" - employmentStatus = 'inactive' → 状态显示为"停薪留职" - 其他 → 状态显示为"在职" ``` --- ## 📊 **六、数据流程图** ``` 页面加载 ↓ ngOnInit() ↓ loadEmployees() ↓ 查询 Profile 表 ├─ 条件:isDeleted != true ├─ 包含:department └─ 排序:name 升序 ↓ 获取 profiles[] ↓ 遍历每个 profile ↓ 提取字段 ├─ 基础字段:name, mobile, email ├─ data.realname ├─ data.wxworkInfo.* └─ data.hrData.* ← 新增的 HR 字段 ↓ 转换为 Employee 对象 ↓ 设置到 employees signal ↓ UI 自动更新 ``` --- ## 🔐 **七、安全特性** ### 敏感信息脱敏 #### 身份证号脱敏 ```typescript maskIdCard(id: string): string { if (!id) return ''; if (id.length >= 18) return `${id.slice(0, 6)}********${id.slice(-4)}`; if (id.length > 6) return `${id.slice(0, 3)}****${id.slice(-2)}`; return id; } // 示例 110105199001011234 → 110105********1234 ``` #### 银行卡号脱敏 ```typescript maskBankCard(card: string): string { if (!card) return ''; const compact = card.replace(/\s+/g, ''); if (compact.length <= 8) return compact; const first4 = compact.slice(0, 4); const last4 = compact.slice(-4); return `${first4} **** **** ${last4}`; } // 示例 6222021234567890 → 6222 **** **** 7890 ``` #### 点击展开/收起 ```typescript toggleSensitive(id: string) { const list = this.sensitiveExpandedIds(); if (list.includes(id)) { this.sensitiveExpandedIds.set(list.filter(x => x !== id)); } else { this.sensitiveExpandedIds.set([...list, id]); } } ``` --- ## ✅ **八、修改后的优势** ### 1. **真实数据** ``` ✅ 从 Profile 表加载真实员工信息 ✅ 与系统其他模块(项目、设计师等)数据一致 ✅ 支持企微数据自动同步 ``` ### 2. **数据完整性** ``` ✅ 新增 HR 专用字段(工号、身份证、银行卡等) ✅ 数据存储在 Profile.data.hrData 中 ✅ 不影响现有字段,向后兼容 ``` ### 3. **安全性** ``` ✅ 敏感信息默认脱敏显示 ✅ 点击展开才显示完整信息 ✅ 符合数据安全规范 ``` ### 4. **可维护性** ``` ✅ 统一数据来源(Profile 表) ✅ 字段优先级清晰 ✅ 详细的日志输出 ✅ 完善的错误处理 ``` --- ## 🧪 **九、测试步骤** ### 1. 加载员工列表 ``` 1. 打开人事板块 → 员工档案 2. 查看控制台日志 ✅ "📋 [员工档案] 开始加载员工数据..." ✅ "✅ [员工档案] 查询到 X 个员工" ✅ "✅ [员工档案] 员工数据加载完成,共 X 个" 3. 验证表格数据显示 - 姓名、部门、职位 - 工号、手机号、邮箱 - 身份证号(脱敏) - 银行卡号(脱敏) - 入职日期、状态 ``` ### 2. 测试敏感信息展开/收起 ``` 1. 默认状态:身份证号和银行卡号脱敏 2. 点击行右侧的眼睛图标 3. 验证:完整信息显示 4. 再次点击:恢复脱敏显示 ``` ### 3. 测试新增员工 ``` 1. 点击"+ 创建档案"按钮 2. 填写表单(包括工号、身份证号、银行卡号) 3. 点击"保存" 4. 验证: ✅ 控制台显示"✅ [员工档案] 员工添加成功" ✅ 列表自动刷新,新员工出现 ✅ Profile 表中新增记录 ✅ data.hrData 字段正确保存 ``` ### 4. 测试编辑员工 ``` 1. 点击员工行的"编辑"按钮 2. 修改信息(如工号、手机号等) 3. 点击"保存" 4. 验证: ✅ 控制台显示"✅ [员工档案] 员工信息更新成功" ✅ 列表自动刷新,显示最新信息 ✅ Profile 表中数据已更新 ``` ### 5. 测试筛选功能 ``` 1. 在搜索框输入员工姓名 2. 选择部门筛选 3. 选择职位筛选 4. 选择状态筛选 5. 验证:列表正确过滤 ``` --- ## 🚨 **十、注意事项** ### 1. **数据迁移** ``` ⚠️ 现有 Profile 记录没有 hrData 字段 ⚠️ 需要人工或脚本批量补充 ⚠️ 无 hrData 时字段显示为空 建议: - 分批次补充员工 HR 数据 - 优先补充在职员工 - 可以通过编辑功能逐个补充 ``` ### 2. **权限控制** ``` ⚠️ 敏感信息(身份证、银行卡)需要权限控制 ⚠️ 当前所有登录用户都可以查看 建议: - 添加角色权限检查 - 只有 HR 和管理员可以查看完整信息 - 其他角色只能看脱敏信息 ``` ### 3. **数据验证** ``` ⚠️ 当前没有强制校验身份证号和银行卡号格式 建议: - 添加身份证号格式验证(18位数字) - 添加银行卡号格式验证(16-19位数字) - 添加手机号格式验证(11位数字) ``` ### 4. **性能优化** ``` ⚠️ 当前一次性加载所有员工(limit 1000) 建议: - 员工数量超过100时,考虑分页加载 - 添加虚拟滚动 - 添加缓存机制 ``` --- ## 📚 **十一、相关文档** ``` 1. Profile 表结构说明 - 位置:数据库设计文档 2. 企微数据同步机制 - 位置:ProfileService 文档 3. HR 模块整体架构 - 位置:人事系统设计文档 ``` --- ## 🎯 **十二、后续优化** ### 优先级 P0(必须) ``` ✅ 已完成:对接 Profile 表真实数据 ✅ 已完成:新增 HR 专用字段 ✅ 已完成:敏感信息脱敏 ``` ### 优先级 P1(重要) ``` 🚧 数据批量导入功能 🚧 身份证号和银行卡号格式验证 🚧 HR 角色权限控制 🚧 离职流程完善 ``` ### 优先级 P2(次要) ``` ⏳ 员工照片上传 ⏳ 合同管理 ⏳ 考勤记录 ⏳ 薪资管理 ⏳ 绩效评估 ``` --- **文档版本**:v1.0 **最后更新**:2025-11-19 01:50 **维护人**:Cascade AI Assistant **状态**:✅ 已完成对接真实数据