SPACE_SYNC_ANALYSIS_PART2.md 14 KB

项目空间数量同步分析报告 - 第二部分

4. 实现细节

4.1 ProductSpaceService 核心方法

文件src/modules/project/services/product-space.service.ts

/**
 * 获取项目的所有空间产品
 * @param projectId 项目ID
 * @returns ProductSpace数组
 */
async getProjectProductSpaces(projectId: string): Promise<ProductSpace[]> {
  const query = new Parse.Query('Product');
  query.equalTo('project', projectId);
  query.notEqualTo('isDeleted', true);
  query.ascending('order');
  return await query.find();
}

/**
 * 创建项目空间产品
 * @param projectId 项目ID
 * @param spaceData 空间数据
 * @returns 创建的ProductSpace
 */
async createProductSpace(
  projectId: string,
  spaceData: Partial<ProductSpace>
): Promise<ProductSpace> {
  const Product = Parse.Object.extend('Product');
  const product = new Product();
  
  product.set('name', spaceData.name);
  product.set('type', spaceData.type);
  product.set('project', projectId);
  product.set('company', localStorage.getItem('company'));
  product.set('status', spaceData.status || 'not_started');
  product.set('complexity', spaceData.complexity || 'medium');
  product.set('order', spaceData.order || 0);
  
  return await product.save();
}

/**
 * 更新项目空间产品
 * @param spaceId 空间ID
 * @param updates 更新数据
 */
async updateProductSpace(
  spaceId: string,
  updates: Partial<ProductSpace>
): Promise<void> {
  const query = new Parse.Query('Product');
  const product = await query.get(spaceId);
  
  Object.keys(updates).forEach(key => {
    product.set(key, (updates as any)[key]);
  });
  
  await product.save();
}

/**
 * 删除项目空间产品(软删除)
 * @param spaceId 空间ID
 */
async deleteProductSpace(spaceId: string): Promise<void> {
  const query = new Parse.Query('Product');
  const product = await query.get(spaceId);
  product.set('isDeleted', true);
  await product.save();
}

/**
 * 计算空间进度
 * @param spaceId 空间ID
 * @param processTypes 工序类型列表
 * @returns 进度百分比
 */
calculateProductProgress(spaceId: string, processTypes: string[]): number {
  // 计算该空间的完成进度
  // 基于关联的ProjectDeliverable记录
  // 返回 0-100 之间的百分比
}

4.2 数据同步触发点

订单分配阶段

// 1. 组件初始化时
ngOnInit() {
  await this.loadProjectSpaces();
}

// 2. 项目类型改变时
onProjectTypeChange() {
  this.quotation.spaces = [];
  this.quotation.total = 0;
}

// 3. 空间模式改变时
async onProjectSpaceModeChange() {
  this.isMultiSpaceProject = this.projectInfo.spaceType === 'multi';
  await this.regenerateQuotationFromSpaces();
}

// 4. 添加/编辑/删除空间时
async addSpace() { ... }
async editSpace(spaceId: string) { ... }
async deleteSpace(spaceId: string) { ... }

交付执行阶段

// 1. 组件初始化时
ngOnInit() {
  await this.syncProductsWithQuotation();
}

// 2. 项目数据变化时
ngOnChanges(changes: SimpleChanges) {
  if (changes['project']) {
    await this.syncProductsWithQuotation();
  }
}

设计师分配弹窗

// 1. 弹窗打开时
async ngOnInit() {
  if (this.loadRealSpaces && this.projectId) {
    await this.loadRealProjectSpaces();
  }
}

// 2. 输入属性变化时
async ngOnChanges(changes: SimpleChanges) {
  if (changes['visible'] || changes['isVisible']) {
    if (currentVisible && !previousVisible && this.loadRealData) {
      await this.enrichMembersWithProjectAssignments();
    }
  }
}

4.3 空间分配数据持久化

设计师分配结果结构

export interface DesignerAssignmentResult {
  selectedDesigners: Designer[];           // 选中的设计师
  primaryTeamId: string;                   // 主要项目组ID
  crossTeamCollaborators: Designer[];      // 跨组合作设计师
  quotationAssignments: any[];             // 报价分配
  spaceAssignments: DesignerSpaceAssignment[]; // ⭐ 空间分配
  projectLeader?: Designer;                // 项目负责人
}

// 空间分配数据
spaceAssignments: [
  {
    designerId: "designer-1",
    designerName: "张设计师",
    spaceIds: ["space-1", "space-2", "space-3"]
  },
  {
    designerId: "designer-2",
    designerName: "李设计师",
    spaceIds: ["space-4", "space-5"]
  }
]

保存位置

  • 临时存储:组件内存中的 designerSpaceMap: Map<designerId, spaceIds[]>
  • 持久化:需要通过 confirm 事件传递给父组件,由父组件保存到 ProjectTeamProject

5. 建议的改进方案

5.1 确保数据一致性

问题:报价修改后未同步Product表

解决方案

// 在quotation-editor中添加同步方法
async saveQuotation(): Promise<void> {
  // 1. 保存报价数据
  await this.saveQuotationData();
  
  // 2. ⭐ 同步Product表
  await this.syncQuotationToProducts();
}

private async syncQuotationToProducts(): Promise<void> {
  const quotationSpaces = this.quotation.spaces;
  const existingProducts = await this.productSpaceService
    .getProjectProductSpaces(this.projectId);
  
  // 找出需要创建的空间
  const existingNames = new Set(
    existingProducts.map(p => (p.name || '').trim().toLowerCase())
  );
  
  for (const space of quotationSpaces) {
    const spaceName = (space.name || '').trim().toLowerCase();
    if (!existingNames.has(spaceName)) {
      await this.productSpaceService.createProductSpace(
        this.projectId,
        {
          name: space.name,
          type: 'other',
          status: 'not_started',
          complexity: 'medium'
        }
      );
    }
  }
}

5.2 添加空间同步验证

问题:无法检测空间数据不一致

解决方案

// 添加同步检查方法
async validateSpaceSync(): Promise<{
  isConsistent: boolean;
  productCount: number;
  quotationCount: number;
  missingInProduct: string[];
  missingInQuotation: string[];
}> {
  const products = await this.productSpaceService
    .getProjectProductSpaces(this.projectId);
  
  const quotationSpaces = this.project.get('data')?.quotation?.spaces || [];
  
  const productNames = new Set(
    products.map(p => (p.name || '').trim().toLowerCase())
  );
  
  const quotationNames = new Set(
    quotationSpaces.map(s => (s.name || '').trim().toLowerCase())
  );
  
  const missingInProduct = quotationSpaces
    .filter(s => !productNames.has((s.name || '').trim().toLowerCase()))
    .map(s => s.name);
  
  const missingInQuotation = products
    .filter(p => !quotationNames.has((p.name || '').trim().toLowerCase()))
    .map(p => p.name);
  
  return {
    isConsistent: missingInProduct.length === 0 && missingInQuotation.length === 0,
    productCount: products.length,
    quotationCount: quotationSpaces.length,
    missingInProduct,
    missingInQuotation
  };
}

5.3 改进设计师空间分配的持久化

问题:空间分配结果未保存到数据库

解决方案

// 在ProjectTeam表中添加空间分配字段
interface ProjectTeamData {
  // ... 其他字段
  spaceAssignments?: {
    [designerId: string]: string[];  // 设计师ID -> 空间ID列表
  };
}

// 保存空间分配
async saveSpaceAssignments(
  projectId: string,
  assignments: DesignerSpaceAssignment[]
): Promise<void> {
  const query = new Parse.Query('Project');
  const project = await query.get(projectId);
  
  const data = project.get('data') || {};
  data.designerSpaceAssignments = {};
  
  for (const assignment of assignments) {
    data.designerSpaceAssignments[assignment.designerId] = assignment.spaceIds;
  }
  
  project.set('data', data);
  await project.save();
}

// 加载空间分配
async loadSpaceAssignments(projectId: string): Promise<Map<string, string[]>> {
  const query = new Parse.Query('Project');
  const project = await query.get(projectId);
  
  const data = project.get('data') || {};
  const assignments = data.designerSpaceAssignments || {};
  
  return new Map(Object.entries(assignments));
}

6. 完整的数据流图

┌─────────────────────────────────────────────────────────────────┐
│                       项目空间数据流                              │
└─────────────────────────────────────────────────────────────────┘

订单分配阶段
├─ 输入:项目ID
├─ 加载Product表 → projectSpaces[]
├─ 同步报价数据 → 创建缺失的Product
└─ 输出:quotation.spaces[]
   └─ 每个空间包含 spaceId(指向Product.id)

        ↓ 保存到 Project.data.quotation

报价明细
├─ 读取:Project.data.quotation.spaces[]
├─ 显示:空间列表和价格明细
└─ 修改:编辑空间信息
   └─ 问题:修改后未同步Product表 ⚠️

        ↓ 项目进入交付执行阶段

交付执行
├─ 加载Product表 → projectProducts[]
├─ 同步报价中的缺失空间 → 创建Product
├─ 去重处理 → 按名称去重
└─ 显示:交付物列表

        ↓ 设计师分配

设计师分配弹窗
├─ 加载Product表 → spaceScenes[]
├─ 转换格式 → SpaceScene[]
├─ 分配空间给设计师 → designerSpaceMap
└─ 输出:DesignerAssignmentResult
   └─ spaceAssignments: DesignerSpaceAssignment[]
      └─ 问题:未保存到数据库 ⚠️

7. 字段存储位置总结表

数据项 存储表 字段路径 数据类型 备注
空间名称 Product name string 如"客厅"、"卧室"
空间类型 Product type string living_room, bedroom等
空间面积 Product area number 单位:㎡
空间状态 Product status string not_started, in_progress等
空间复杂度 Product complexity string low, medium, high
空间预算 Product estimatedBudget number 预算金额
空间排序 Product order number 排序顺序
项目关联 Product project Pointer 指向Project表
公司关联 Product company Pointer 指向Company表
报价信息 Project data.quotation Object 包含spaces数组
报价空间 Project data.quotation.spaces[] Array 每个空间包含spaceId
特殊需求 Project data.spaceSpecialRequirements Object 按spaceId存储
空间分配 Project data.designerSpaceAssignments Object 设计师ID -> 空间ID[]

8. 关键函数速查表

8.1 ProductSpaceService

// 查询空间
getProjectProductSpaces(projectId: string): Promise<ProductSpace[]>

// 创建空间
createProductSpace(projectId: string, spaceData: Partial<ProductSpace>): Promise<ProductSpace>

// 更新空间
updateProductSpace(spaceId: string, updates: Partial<ProductSpace>): Promise<void>

// 删除空间
deleteProductSpace(spaceId: string): Promise<void>

// 计算进度
calculateProductProgress(spaceId: string, processTypes: string[]): number

8.2 stage-order.component.ts

// 加载空间
loadProjectSpaces(): Promise<void>

// 从报价创建空间
createSpacesFromQuotation(quotationSpaces: any[]): Promise<void>

// 推断空间类型
inferSpaceType(spaceName: string): string

// 计算空间预算
calculateSpaceRate(spaceData: any): number

// 从空间生成报价
regenerateQuotationFromSpaces(): Promise<void>

// 更新空间占比
updateSpaceBreakdown(): void

8.3 stage-delivery.component.ts

// 同步报价中的空间到Product表
syncProductsWithQuotation(): Promise<void>

8.4 designer-team-assignment-modal.component.ts

// 加载真实空间数据
loadRealProjectSpaces(): Promise<void>

// 获取设计师分配的空间
getDesignerSpaces(designerId: string): SpaceScene[]

// 获取设计师空间文本
getDesignerSpacesText(designerId: string): string

// 检查空间是否被选中
isSpaceSelected(designerId: string, spaceId: string): boolean

// 切换空间选择
toggleSpaceSelection(designerId: string, spaceId: string): void

9. 常见问题排查

Q1: 为什么设计师分配弹窗显示的空间数量与报价不一致?

原因

  1. 报价中添加了新空间,但未同步到Product表
  2. Product表中的空间被删除,但报价未更新

解决

// 在保存报价时调用
await this.syncQuotationToProducts();

// 或在交付执行时自动同步
await this.syncProductsWithQuotation();

Q2: 如何确保空间数据的一致性?

方案

// 定期验证
const validation = await this.validateSpaceSync();
if (!validation.isConsistent) {
  console.warn('空间数据不一致:', validation);
  // 自动修复
  await this.syncQuotationToProducts();
}

Q3: 设计师空间分配如何持久化?

方案

// 在确认分配时保存
async onConfirmAssignment(result: DesignerAssignmentResult) {
  // 保存空间分配
  await this.saveSpaceAssignments(this.projectId, result.spaceAssignments);
}

10. 总结

核心要点:

  1. Product表是空间数据的唯一真实来源
  2. Project.data.quotation存储报价信息
  3. ✅ 各阶段通过ProductSpaceService统一查询空间
  4. ⚠️ 报价修改后需要同步Product表
  5. ⚠️ 设计师分配结果需要持久化存储

改进优先级:

  1. :添加报价保存时的Product同步
  2. :实现设计师空间分配的持久化
  3. :添加空间数据一致性验证
  4. :完善错误处理和日志

相关文件:

  • src/modules/project/services/product-space.service.ts
  • src/modules/project/pages/project-detail/stages/stage-order.component.ts
  • src/modules/project/pages/project-detail/stages/stage-delivery.component.ts
  • src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts