2025-11-19 01:50
将人事板块的员工档案列表从硬编码假数据切换到 Profile 表的真实数据,并在 Profile.data.hrData 中添加 HR 专用字段。
Profile {
id: string; // 员工ID
name: string; // 昵称(企微昵称)
mobile: string; // 手机号
email: string; // 邮箱
roleName: string; // 角色名称
department: Pointer<Department>; // 所属部门
isDeleted: boolean; // 是否删除
isDisabled: boolean; // 是否禁用(离职)
createdAt: Date; // 创建时间
}
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 字段 | 优先级 | 说明 |
|---|---|---|---|
| 姓名 | 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 专用字段,脱敏显示 |
if (profile.isDisabled) {
status = '离职';
} else if (hrData.employmentStatus === 'probation') {
status = '试用期';
} else if (hrData.employmentStatus === 'inactive') {
status = '停薪留职';
} else {
status = '在职'; // 默认
}
文件:employee-records.ts
import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
const Parse = FmodeParse.with('nova');
原代码(硬编码假数据):
employees = signal<Employee[]>([
{
id: '1',
name: '张三',
department: '设计部',
// ... 硬编码数据
}
]);
新代码(从 Profile 表加载):
// 员工数据 - 从 Profile 表加载
employees = signal<Employee[]>([]);
// 加载状态
loading = signal(false);
loadError = signal('');
文件:employee-records.ts
/**
* 从 Profile 表加载真实的员工数据
*/
async loadEmployees(): Promise<void> {
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);
}
}
文件:employee-records.ts
/**
* 保存员工 HR 数据到 Profile.data.hrData
*/
async saveEmployeeHRData(profileId: string, hrData: any): Promise<void> {
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;
}
}
原代码(更新本地数组):
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 表):
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');
}
}
});
类似的修改,将本地数组更新改为 Profile 表更新:
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');
}
}
});
ngOnInit() {
this.loadEmployees();
}
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'
});
}
{
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': 停薪留职
}
用途:唯一标识员工,用于考勤、薪资等
格式:部门缩写 + 序号(如:DS001)
示例:
- DS001: 设计部001号
- CS002: 客服部002号
- FE001: 前端开发001号
用途:员工身份验证,用于社保、公积金等
格式:18位身份证号
安全:界面脱敏显示(前6位+****+后4位)
示例:110105199001011234
显示:110105********1234
用途:工资发放
格式:16-19位银行卡号
安全:界面脱敏显示(前4位+****+后4位)
示例:6222021234567890
显示:6222 **** **** 7890
用途:员工年龄计算、生日提醒
格式:Date 对象或 ISO 字符串
示例:new Date('1990-01-01')
用途:工龄计算、试用期管理
格式:Date 对象或 ISO 字符串
示例:new Date('2022-01-15')
备注:无此字段时使用 Profile.createdAt
值:
- '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 自动更新
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
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
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]);
}
}
✅ 从 Profile 表加载真实员工信息
✅ 与系统其他模块(项目、设计师等)数据一致
✅ 支持企微数据自动同步
✅ 新增 HR 专用字段(工号、身份证、银行卡等)
✅ 数据存储在 Profile.data.hrData 中
✅ 不影响现有字段,向后兼容
✅ 敏感信息默认脱敏显示
✅ 点击展开才显示完整信息
✅ 符合数据安全规范
✅ 统一数据来源(Profile 表)
✅ 字段优先级清晰
✅ 详细的日志输出
✅ 完善的错误处理
1. 打开人事板块 → 员工档案
2. 查看控制台日志
✅ "📋 [员工档案] 开始加载员工数据..."
✅ "✅ [员工档案] 查询到 X 个员工"
✅ "✅ [员工档案] 员工数据加载完成,共 X 个"
3. 验证表格数据显示
- 姓名、部门、职位
- 工号、手机号、邮箱
- 身份证号(脱敏)
- 银行卡号(脱敏)
- 入职日期、状态
1. 默认状态:身份证号和银行卡号脱敏
2. 点击行右侧的眼睛图标
3. 验证:完整信息显示
4. 再次点击:恢复脱敏显示
1. 点击"+ 创建档案"按钮
2. 填写表单(包括工号、身份证号、银行卡号)
3. 点击"保存"
4. 验证:
✅ 控制台显示"✅ [员工档案] 员工添加成功"
✅ 列表自动刷新,新员工出现
✅ Profile 表中新增记录
✅ data.hrData 字段正确保存
1. 点击员工行的"编辑"按钮
2. 修改信息(如工号、手机号等)
3. 点击"保存"
4. 验证:
✅ 控制台显示"✅ [员工档案] 员工信息更新成功"
✅ 列表自动刷新,显示最新信息
✅ Profile 表中数据已更新
1. 在搜索框输入员工姓名
2. 选择部门筛选
3. 选择职位筛选
4. 选择状态筛选
5. 验证:列表正确过滤
⚠️ 现有 Profile 记录没有 hrData 字段
⚠️ 需要人工或脚本批量补充
⚠️ 无 hrData 时字段显示为空
建议:
- 分批次补充员工 HR 数据
- 优先补充在职员工
- 可以通过编辑功能逐个补充
⚠️ 敏感信息(身份证、银行卡)需要权限控制
⚠️ 当前所有登录用户都可以查看
建议:
- 添加角色权限检查
- 只有 HR 和管理员可以查看完整信息
- 其他角色只能看脱敏信息
⚠️ 当前没有强制校验身份证号和银行卡号格式
建议:
- 添加身份证号格式验证(18位数字)
- 添加银行卡号格式验证(16-19位数字)
- 添加手机号格式验证(11位数字)
⚠️ 当前一次性加载所有员工(limit 1000)
建议:
- 员工数量超过100时,考虑分页加载
- 添加虚拟滚动
- 添加缓存机制
1. Profile 表结构说明
- 位置:数据库设计文档
2. 企微数据同步机制
- 位置:ProfileService 文档
3. HR 模块整体架构
- 位置:人事系统设计文档
✅ 已完成:对接 Profile 表真实数据
✅ 已完成:新增 HR 专用字段
✅ 已完成:敏感信息脱敏
🚧 数据批量导入功能
🚧 身份证号和银行卡号格式验证
🚧 HR 角色权限控制
🚧 离职流程完善
⏳ 员工照片上传
⏳ 合同管理
⏳ 考勤记录
⏳ 薪资管理
⏳ 绩效评估
文档版本:v1.0
最后更新:2025-11-19 01:50
维护人:Cascade AI Assistant
状态:✅ 已完成对接真实数据