日期: 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! 🚀