日期: 2025-10-24
核心思路: 不修改数据库,只通过 department.leader 动态获取组长信息
Project表
├─ assignee: Pointer<Profile>  ← 需要迁移数据
├─ department: Pointer<Department>
└─ ...
缺点:
Project表
├─ department: Pointer<Department>  ← 只需要这个
└─ ...
Department表
├─ leader: Pointer<Profile>  ← 从这里获取组长
└─ ...
优点:
文件: src/app/pages/admin/services/project.service.ts
已修改(第27行):
async findProjects(options?: {
  status?: string;
  keyword?: string;
  skip?: number;
  limit?: number;
}): Promise<FmodeObject[]> {
  return await this.adminData.findAll('Project', {
    include: ['customer', 'assignee', 'department', 'department.leader'], // ✅ 包含leader
    skip: options?.skip || 0,
    limit: options?.limit || 20,
    descending: 'updatedAt',
    // ...
  });
}
文件: src/app/pages/admin/services/project.service.ts
已修改(第243-256行):
toJSON(project: FmodeObject): any {
  const json = this.adminData.toJSON(project);
  // 处理客户
  if (json.customer && typeof json.customer === 'object') {
    json.customerName = json.customer.name || '';
    json.customerId = json.customer.objectId;
  }
  // ✅ 处理负责人:优先使用assignee,如果为空则使用department.leader
  if (json.assignee && typeof json.assignee === 'object') {
    // 如果有明确指定的assignee,使用它
    json.assigneeName = json.assignee.name || '';
    json.assigneeId = json.assignee.objectId;
    json.assigneeRole = json.assignee.roleName || '';
  } else if (json.department && typeof json.department === 'object') {
    // 如果没有assignee,使用department的leader(组长)
    const leader = json.department.leader;
    if (leader && typeof leader === 'object') {
      json.assigneeName = leader.name || '';
      json.assigneeId = leader.objectId;
      json.assigneeRole = '组长';  // ✅ 标记为组长
    }
  }
  return json;
}
检查方法(在浏览器控制台执行):
(async function() {
  const Parse = window.FmodeParse.with('nova');
  const company = localStorage.getItem('company');
  
  const query = new Parse.Query('Project');
  query.equalTo('company', company);
  query.notEqualTo('isDeleted', true);
  query.include('department', 'department.leader');
  query.limit(20);
  
  const projects = await query.find();
  
  let hasDept = 0;
  let noDept = 0;
  
  projects.forEach(p => {
    if (p.get('department')) {
      hasDept++;
    } else {
      noDept++;
    }
  });
  
  console.log(`✅ 有department: ${hasDept} 个`);
  console.log(`❌ 没有department: ${noDept} 个`);
  
  if (noDept > 0) {
    console.log('\n⚠️ 有项目没有department,需要分配项目组');
  } else {
    console.log('\n🎉 所有项目都有department,可以直接显示组长!');
  }
})();
(async function() {
  const Parse = window.FmodeParse.with('nova');
  const company = localStorage.getItem('company');
  
  console.log('🚀 开始为项目分配默认项目组...');
  
  // 1. 查找没有department的项目
  const projectQuery = new Parse.Query('Project');
  projectQuery.equalTo('company', company);
  projectQuery.notEqualTo('isDeleted', true);
  projectQuery.doesNotExist('department');
  projectQuery.limit(1000);
  
  const projects = await projectQuery.find();
  console.log(`📊 找到 ${projects.length} 个没有项目组的项目`);
  
  if (projects.length === 0) {
    console.log('✅ 所有项目都已有项目组!');
    return;
  }
  
  // 2. 获取默认项目组(第一个项目组)
  const deptQuery = new Parse.Query('Department');
  deptQuery.equalTo('company', company);
  deptQuery.equalTo('type', 'project');
  deptQuery.notEqualTo('isDeleted', true);
  deptQuery.include('leader');
  deptQuery.ascending('createdAt');
  
  const dept = await deptQuery.first();
  
  if (!dept) {
    console.error('❌ 没有找到项目组,请先创建项目组');
    return;
  }
  
  const leader = dept.get('leader');
  console.log(`✅ 使用默认项目组: ${dept.get('name')}, 组长: ${leader?.get('name') || '无'}`);
  
  // 3. 批量分配
  let success = 0;
  let failed = 0;
  
  for (let i = 0; i < projects.length; i++) {
    const project = projects[i];
    const title = project.get('title') || '未命名项目';
    
    try {
      project.set('department', dept);
      await project.save();
      success++;
      console.log(`✅ [${i+1}/${projects.length}] "${title}" 已分配到 ${dept.get('name')}`);
    } catch (error) {
      failed++;
      console.error(`❌ [${i+1}/${projects.length}] "${title}" 失败:`, error);
    }
  }
  
  console.log('\n' + '='.repeat(60));
  console.log('🎉 分配完成!');
  console.log('='.repeat(60));
  console.log(`📊 总计: ${projects.length} | ✅ 成功: ${success} | ❌ 失败: ${failed}`);
  console.log('='.repeat(60));
  console.log('\n💡 请刷新页面(Ctrl+Shift+R)查看结果');
})();
打开 http://localhost:4200/admin/project-management,按 F12,执行:
(async function() {
  const Parse = window.FmodeParse.with('nova');
  const company = localStorage.getItem('company');
  
  const query = new Parse.Query('Project');
  query.equalTo('company', company);
  query.include('department', 'department.leader');
  query.limit(20);
  
  const projects = await query.find();
  
  console.table(projects.map(p => ({
    '项目名称': p.get('title'),
    '有项目组': p.get('department') ? '✅' : '❌',
    '组长': p.get('department')?.get('leader')?.get('name') || '无'
  })));
})();
复制上面的"为项目分配默认项目组"脚本,粘贴到控制台执行。
按 Ctrl+Shift+R 刷新页面,查看项目列表的"负责人"列。
预期结果:
{
  objectId: "APwk78jnrh",
  title: "张家界凤凰城三期项目",
  department: Pointer<Department> { objectId: "xxx" },  // ✅ 只需要这个
  company: "cDL6R1hgSi",
  status: "进行中",
  // 不需要 assignee 字段
}
{
  objectId: "xxx",
  name: "汪奥组",
  type: "project",
  leader: Pointer<Profile> { objectId: "yyy" },  // ✅ 组长信息存在这里
  company: "cDL6R1hgSi"
}
{
  objectId: "yyy",
  name: "汪奥",
  roleName: "组长",
  company: "cDL6R1hgSi"
}
| 对比项 | 方案1(存储assignee) | 方案2(使用leader)✅ | 
|---|---|---|
| 需要迁移数据 | ❌ 是 | ✅ 否 | 
| 数据一致性 | ❌ 可能不一致 | ✅ 始终一致 | 
| 逻辑复杂度 | ❌ 较复杂 | ✅ 简单 | 
| 查询性能 | ✅ 稍快 | ✅ 同样快 | 
| 维护成本 | ❌ 高 | ✅ 低 | 
逻辑清晰
数据一致性
无需迁移
department 字段assignee 字段符合业务逻辑
assignee 字段现在执行检查脚本,看看是否所有项目都有 department! 🚀