2025-11-20 00:34
❌ 原来:硬编码假数据
✅ 现在:从 Profile 表加载真实员工数据
✅ 只显示已激活的员工(isActivated = true)
✅ 添加身份证号字段(带格式验证)
✅ 添加银行卡号字段(带格式验证)
✅ 添加"停薪留职"状态选项
✅ 所有字段正确映射到 Profile 表
身份证号:18位,支持数字和X结尾
正则:/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
银行卡号:16-19位数字
正则:/^\d{16,19}$/
| 界面显示字段 | Profile 表字段 | 优先级顺序 | 代码位置 |
|---|---|---|---|
| 姓名 | data.realname → wxworkInfo.name → name |
真实姓名第一优先 | 第664行 |
| 工号 | data.hrData.employeeId |
HR专用字段 | 第667行 |
| 部门 | department.name |
关联查询 | 第665行 |
| 职位 | wxworkInfo.position → data.position → hrData.position → roleName |
多来源 | 第666行 |
| 手机号 | wxworkInfo.mobile → data.mobile → mobile |
企微优先 | 第668行 |
| 邮箱 | wxworkInfo.email → data.email → email |
企微优先 | 第669行 |
| 性别 | wxworkInfo.gender → data.gender → hrData.gender |
企微优先 | 第670行 |
| 出生日期 | data.hrData.birthDate |
HR专用字段 | 第671行 |
| 入职日期 | data.hrData.hireDate → createdAt |
HR字段优先 | 第672行 |
| 状态 | 根据 isDisabled + hrData.employmentStatus 计算 |
计算字段 | 第653-660行 |
| 身份证号 | data.hrData.idCard |
HR专用字段(脱敏) | 第673行 |
| 银行卡号 | data.hrData.bankCard |
HR专用字段(脱敏) | 第674行 |
{
id: string; // 员工ID
userid: string; // 企微用户ID
name: string; // 昵称
realname: string; // 真实姓名 ⭐ 第一优先
mobile: string; // 手机号
email: string; // 邮箱
roleName: string; // 角色
department: Pointer<Department>; // 部门关联
isActivated: boolean; // 是否已激活 ✅ 过滤条件
isDeleted: boolean; // 是否删除
isDisabled: boolean; // 是否禁用(离职)
createdAt: Date; // 创建时间
}
{
realname: string; // 真实姓名(备份)
mobile: string; // 手机号(备份)
email: string; // 邮箱(备份)
gender: 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; // 出生日期
hireDate: Date; // 入职日期
gender: string; // 性别
position: string; // 职位
employmentStatus: string; // 就业状态
// - 'active': 在职
// - 'probation': 试用期
// - 'inactive': 停薪留职
}
}
// 第630-635行
const query = new Parse.Query('Profile');
query.equalTo('isActivated', true); // ✅ 只查询已激活员工
query.notEqualTo('isDeleted', true); // 排除已删除
query.include('department'); // 包含部门信息
query.ascending('realname'); // 按真实姓名排序
query.limit(1000); // 限制1000条
const profiles = await query.find();
// 第653-660行
let status: EmployeeStatus = '在职'; // 默认状态
if (profile.get('isDisabled')) {
status = '离职'; // 已禁用 = 离职
} else if (hrData.employmentStatus === 'probation') {
status = '试用期'; // 试用期
} else if (hrData.employmentStatus === 'inactive') {
status = '停薪留职'; // 停薪留职
}
{
name: string; // 姓名(必填)
employeeId: string; // 工号(必填)
department: string; // 部门(必填)
position: string; // 职位(必填)
phone: string; // 手机号(必填,格式验证)
email: string; // 邮箱(必填,格式验证)
gender: string; // 性别(必填)
birthDate: Date; // 出生日期(可选)
hireDate: Date; // 入职日期(必填)
status: string; // 状态(必填)
idCard: string; // 身份证号(可选,格式验证)✅ 新增
bankCard: string; // 银行卡号(可选,格式验证)✅ 新增
}
// 身份证号验证(可选,但如果填写则必须符合格式)
idCard: ['', [Validators.pattern(/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/)]]
// 银行卡号验证(可选,但如果填写则必须符合格式)
bankCard: ['', [Validators.pattern(/^\d{16,19}$/)]]
// 第746-784行
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();
// 第808-848行
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();
// 第702-707行
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
// 第709-716行
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
// 第727-733行
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. 打开:人事板块 → 员工档案管理
2. 查看控制台日志:
✅ "📋 [员工档案] 开始加载员工数据..."
✅ "✅ [员工档案] 查询到 X 个员工"
3. 验证:只显示已激活(isActivated=true)的员工
4. 验证:姓名显示的是 Profile.data.realname
1. 点击员工行的"操作" → "编辑"
2. 验证:编辑对话框包含所有字段
✅ 姓名、工号、部门、职位
✅ 手机号、邮箱、性别
✅ 出生日期、入职日期、状态
✅ 身份证号、银行卡号 ⭐ 新增
3. 修改信息,点击"保存"
4. 验证:
✅ 控制台显示成功消息
✅ 列表自动刷新
✅ Profile 表数据已更新
1. 点击"新增员工"按钮
2. 填写所有必填字段
3. 填写身份证号和银行卡号
4. 点击"保存"
5. 验证:
✅ 新员工出现在列表中
✅ Profile 表中有新记录
✅ data.hrData 字段正确保存
1. 编辑员工
2. 输入错误的身份证号(如:12345)
✅ 显示错误提示:"请输入有效的18位身份证号"
3. 输入错误的银行卡号(如:12345)
✅ 显示错误提示:"请输入有效的银行卡号"
4. 输入正确格式才能保存
1. 查看身份证号:默认脱敏
2. 查看银行卡号:默认脱敏
3. 点击眼睛图标:展开完整信息
4. 再次点击:恢复脱敏
✅ 已实现:query.equalTo('isActivated', true)
⚠️ 未激活的员工不会出现在列表中
⚠️ 如果没有员工显示,请检查 Profile.isActivated 字段
⚠️ 现有 Profile 记录可能没有 hrData 字段
⚠️ 工号、身份证号、银行卡号等字段会显示为空
建议:
1. 通过编辑功能逐个补充员工 HR 数据
2. 优先补充在职员工的信息
3. 或编写数据迁移脚本批量导入
⚠️ 部门是通过 Pointer 关联的
⚠️ 保存时会查询 Department 表
⚠️ 如果部门不存在,需要先在 Department 表中创建
⚠️ 当前所有登录用户都可以查看敏感信息
⚠️ 建议添加 HR 角色权限控制
⚠️ 只有 HR 和管理员可以查看完整信息
| 文件 | 修改内容 |
|---|---|
employee-records.ts |
✅ 添加身份证号和银行卡号字段到编辑对话框 |
| ↓ | ✅ 添加字段格式验证 |
| ↓ | ✅ 添加"停薪留职"状态选项 |
| ↓ | ✅ 修改查询条件:只显示已激活员工 |
| ↓ | ✅ 按 realname 字段排序 |
hr.model.ts |
✅ 添加"停薪留职"到 EmployeeStatus 类型 |
✅ 从 Profile 表加载真实数据
✅ 只显示已激活的员工(isActivated = true)
✅ 姓名使用 data.realname 字段(第一优先级)
✅ 部门、职位、手机号等字段正确映射
✅ 身份证号和银行卡号正确显示和保存
✅ 敏感信息脱敏显示
✅ 点击眼睛图标可展开/收起敏感信息
✅ 编辑对话框包含所有必要字段
✅ 新增员工功能正常工作
✅ 数据保存到 Profile.data.hrData
✅ 格式验证正常工作
✅ TypeScript 编译无错误
页面加载
↓
ngOnInit() → loadEmployees()
↓
查询 Profile 表
├─ isActivated = true ✅ 只查已激活
├─ isDeleted != true
├─ include: department
└─ 按 realname 排序
↓
获取 profiles[]
↓
遍历每个 profile
↓
提取字段
├─ 直接字段:id, realname, mobile, email
├─ data.realname ⭐ 第一优先
├─ data.wxworkInfo.*
└─ data.hrData.* ✅ HR专用字段
↓
转换为 Employee[]
↓
设置到 employees signal
↓
UI 自动更新
↓
显示员工列表
├─ 姓名(realname)
├─ 工号(hrData.employeeId)
├─ 部门、职位、手机号
├─ 身份证号(脱敏)
├─ 银行卡号(脱敏)
└─ 入职日期、状态
文档版本:v2.0
最后更新:2025-11-20 00:34
维护人:Cascade AI Assistant
状态:✅ 已完成真实数据对接和编辑功能完善