SPACE_SYNC_INCONSISTENCY_ANALYSIS.md 14 KB

报价空间同步不一致问题分析

🎯 用户反馈的问题

现象:不同项目的报价空间计算方式不一致

  • 有的项目可以同步删除更新
  • 有的项目不能同步删除更新

🔍 深度分析

核心问题:数据同步的完整性不一致

经过代码审查,我发现了以下关键问题导致不同项目行为不一致:


❌ 问题1:订单分配阶段缺少事件监听

当前状态

报价编辑器 (quotation-editor.component.ts)

// ✅ 有广播事件
private broadcastProductUpdate(): void {
  const event = new CustomEvent('product-spaces-updated', {
    detail: { projectId, productCount, timestamp }
  });
  document.dispatchEvent(event);
}

确认需求阶段 (stage-requirements.component.ts)

// ✅ 有监听事件
private setupProductUpdateListener(): void {
  this.productUpdateListener = (event: any) => {
    if (detail.projectId === this.projectId) {
      this.loadData();  // 重新加载数据
    }
  };
  document.addEventListener('product-spaces-updated', this.productUpdateListener);
}

交付执行阶段 (stage-delivery.component.ts)

// ✅ 有监听事件
private setupProductUpdateListener(): void {
  this.productUpdateListener = (event: any) => {
    if (detail.projectId === this.projectId) {
      this.loadData();  // 重新加载数据
    }
  };
  document.addEventListener('product-spaces-updated', this.productUpdateListener);
}

订单分配阶段 (stage-order.component.ts)

// ❌ 没有监听事件!只有广播,没有接收
// 这导致在订单分配阶段,报价编辑器的变更不会触发订单分配页面刷新

问题影响

当用户在订单分配阶段操作报价编辑器时:

  1. 用户删除一个空间
  2. 报价编辑器保存数据到数据库 ✅
  3. 报价编辑器广播 product-spaces-updated 事件 ✅
  4. 订单分配阶段组件没有监听,不会刷新
  5. 用户看到的界面数据是旧的,需要手动刷新页面

❌ 问题2:旧项目缺少 unifiedSpaces 数据

数据迁移问题

旧项目(在修复之前创建的):

project.data = {
  quotation: {
    spaces: [...],  // 只有这个
    total: 300
  }
  // ❌ 没有 unifiedSpaces
}

新项目(修复之后创建的):

project.data = {
  quotation: {
    spaces: [...],
    total: 300
  },
  unifiedSpaces: [...]  // ✅ 有这个
}

问题影响

  • 新项目:各阶段从 unifiedSpaces 读取,数据一致 ✅
  • 旧项目
    • 订单分配:从 quotation.spaces 读取 ✅
    • 确认需求:从 unifiedSpaces 读取(空数组),回退到 Product表查询 ⚠️
    • 交付执行:从 Product表查询 ⚠️
    • 结果:不同阶段看到的数据不一致

❌ 问题3:Product表重复数据未清理

重复产品问题

某些旧项目的Product表存在重复记录:

Product表:
- 餐厅 (ID: abc123)  ← 保留
- 餐厅 (ID: def456)  ← 重复
- 主卧 (ID: ghi789)

当前处理方式

报价编辑器加载时

// ✅ 已有去重逻辑
loadProjectProducts() {
  const seen = new Set<string>();
  for (const p of results) {
    const key = productName.trim().toLowerCase();
    if (!seen.has(key)) {
      seen.add(key);
      this.products.push(p);  // 只保留第一个
    } else {
      console.log('⚠️ 跳过重复空间');  // 跳过重复的
    }
  }
}

问题

  • 只是在内存中跳过重复数据
  • 数据库中的重复Product记录没有被删除
  • 下次加载时又会遇到重复数据

❌ 问题4:同步时机不完整

数据同步调用时机

当前 syncUnifiedSpaces() 的调用位置

  1. loadProjectDataFromProject() - 加载项目数据后
  2. saveQuotationToProject() - 保存报价后

缺失的调用位置

  1. deleteProduct() - 删除产品后没有显式调用(依赖 saveQuotationToProject)
  2. updateExistingProduct() - 更新产品后没有显式调用(依赖 saveQuotationToProject)
  3. createNewProduct() - 创建新产品后没有显式调用(依赖 saveQuotationToProject)

问题影响

虽然这些操作最终都会调用 saveQuotationToProject(),但如果:

  • 保存失败
  • 网络中断
  • 异常抛出

就会导致 unifiedSpaces 没有被同步。


✅ 完整修复方案

修复1:为订单分配阶段添加事件监听

文件stage-order.component.ts

export class StageOrderComponent implements OnInit, OnDestroy {
  // 添加监听器属性
  private productUpdateListener: any = null;

  async ngOnInit() {
    // ... 现有初始化代码 ...
    
    // 🔥 添加产品更新监听器
    this.setupProductUpdateListener();
  }

  ngOnDestroy() {
    // ... 现有清理代码 ...
    
    // 🔥 清理产品更新监听器
    if (this.productUpdateListener) {
      document.removeEventListener('product-spaces-updated', this.productUpdateListener);
      console.log('🧹 [订单分配] 已清理产品更新监听器');
    }
  }

  /**
   * 🔥 设置产品更新监听器
   */
  private setupProductUpdateListener(): void {
    this.productUpdateListener = (event: any) => {
      const detail = event.detail || {};
      if (detail.projectId === this.projectId) {
        console.log('🔔 [订单分配] 收到产品更新事件,重新加载数据...');
        
        // 重新加载报价数据
        this.loadData();
        
        // 强制Angular检测变化
        this.cdr.detectChanges();
      }
    };
    
    document.addEventListener('product-spaces-updated', this.productUpdateListener);
    console.log('✅ [订单分配] 已设置产品更新监听器');
  }
}

修复2:为旧项目自动生成 unifiedSpaces

文件quotation-editor.component.ts

loadProjectDataFromProject() 中增强逻辑:

private async loadProjectDataFromProject(): Promise<void> {
  // ... 加载产品 ...
  await this.loadProjectProducts();

  const data = this.project.get('data') || {};
  
  // 🔥 检测旧项目:没有 unifiedSpaces 但有产品
  if (!data.unifiedSpaces && this.products.length > 0) {
    console.warn('⚠️ [报价编辑器] 检测到旧项目,自动生成 unifiedSpaces...');
    
    // 生成报价(如果没有)
    if (!data.quotation || !data.quotation.spaces) {
      await this.generateQuotationFromProducts();
    }
    
    // 同步 unifiedSpaces
    await this.syncUnifiedSpaces();
    
    console.log('✅ [报价编辑器] 旧项目数据已迁移');
  } else {
    // 正常流程
    if (data.quotation) {
      this.quotation = data.quotation;
      this.updateProductsFromQuotation();
    } else if (this.products.length > 0) {
      await this.generateQuotationFromProducts();
    }
    
    await this.syncUnifiedSpaces();
  }
}

修复3:添加重复Product自动清理工具

文件quotation-editor.component.ts

增强 generateQuotationFromProducts() 方法:

async generateQuotationFromProducts(): Promise<void> {
  // ... 生成报价逻辑 ...
  
  // 🔥 检测到重复产品时,自动提示清理
  if (duplicateProductIds.length > 0) {
    console.warn(`⚠️ 检测到 ${duplicateProductIds.length} 个重复产品`);
    
    // 如果可以编辑,提示用户清理
    if (this.canEdit) {
      const shouldClean = await window?.fmode?.confirm(
        `检测到 ${duplicateProductIds.length} 个重复空间产品,是否自动清理?\n\n清理后将删除重复记录,保留第一个创建的空间。`
      );
      
      if (shouldClean) {
        await this.removeDuplicateProducts(duplicateProductIds);
        return; // 清理后重新生成报价
      }
    }
  }
  
  // ... 继续正常流程 ...
}

修复4:增强错误处理和同步保证

文件quotation-editor.component.ts

在关键操作中添加同步保证:

async deleteProduct(productId: string): Promise<void> {
  try {
    // ... 删除逻辑 ...
    await product.destroy();
    
    // 更新本地数据
    this.products = this.products.filter(p => p.id !== productId);
    this.quotation.spaces = this.quotation.spaces.filter((s: any) => s.productId !== productId);
    
    // 重新计算
    this.calculateTotal();
    this.updateProductBreakdown();
    
    // 🔥 保存报价(会自动调用 syncUnifiedSpaces 和 broadcastProductUpdate)
    await this.saveQuotationToProject();
    
    // 🔥 额外保证:如果 saveQuotationToProject 失败,也要尝试同步
    try {
      await this.syncUnifiedSpaces();
    } catch (syncError) {
      console.error('❌ 同步 unifiedSpaces 失败:', syncError);
    }
    
    console.log('✅ [产品变更] 已删除产品,当前数量:', this.products.length);
    
  } catch (error) {
    console.error('❌ 删除产品失败:', error);
    window?.fmode?.alert('删除失败,请重试');
    
    // 🔥 失败时重新加载数据,确保界面与数据库一致
    await this.loadProjectProducts();
  }
}

🧪 测试验证方案

测试场景1:新项目数据同步

  1. 创建新项目
  2. 在订单分配阶段添加空间
  3. 删除一个空间
  4. 预期:界面立即更新,不需要刷新页面
  5. 切换到确认需求阶段
  6. 预期:空间数量与订单分配一致

测试场景2:旧项目自动迁移

  1. 打开一个旧项目(没有 unifiedSpaces)
  2. 进入订单分配阶段
  3. 预期:控制台显示"检测到旧项目,自动生成 unifiedSpaces"
  4. 切换到确认需求阶段
  5. 预期:空间数量与订单分配一致

测试场景3:重复数据自动清理

  1. 找一个有重复Product的项目
  2. 进入订单分配阶段
  3. 点击"生成报价"
  4. 预期:弹出提示"检测到X个重复空间产品,是否自动清理?"
  5. 点击"确定"
  6. 预期:重复Product被删除,只保留一个

测试场景4:网络中断容错

  1. 断开网络
  2. 在报价编辑器中删除一个空间
  3. 预期:显示保存失败
  4. 恢复网络
  5. 预期:重新加载数据,界面恢复到数据库状态

📊 数据同步流程图

修复后的完整流程

用户操作(添加/编辑/删除空间)
    ↓
报价编辑器处理
    ├─ 更新本地 products 数组
    ├─ 更新本地 quotation.spaces 数组
    └─ 重新计算总价
    ↓
saveQuotationToProject()
    ├─ 保存 data.quotation
    ├─ 同步 data.unifiedSpaces  ← 关键
    ├─ 保存到数据库
    └─ 调用 broadcastProductUpdate()
    ↓
广播全局事件 'product-spaces-updated'
    ↓
┌─────────────────┬─────────────────┬─────────────────┐
│  订单分配阶段    │  确认需求阶段    │  交付执行阶段    │
│  (新增监听)     │  (已有监听)     │  (已有监听)     │
└─────────────────┴─────────────────┴─────────────────┘
    │                   │                   │
    └───────────────────┴───────────────────┘
                        ↓
                  重新加载数据
                        ↓
              界面自动刷新,数据一致 ✅

🔧 实施步骤

第一步:修复订单分配阶段监听(高优先级)

  1. 修改 stage-order.component.ts
  2. 添加 setupProductUpdateListener() 方法
  3. ngOnInit 中调用
  4. ngOnDestroy 中清理

第二步:增强旧项目迁移(高优先级)

  1. 修改 quotation-editor.component.tsloadProjectDataFromProject()
  2. 检测没有 unifiedSpaces 的旧项目
  3. 自动生成 unifiedSpaces

第三步:添加重复数据清理(中优先级)

  1. 增强 generateQuotationFromProducts() 方法
  2. 检测重复Product时提示用户
  3. 提供一键清理功能

第四步:增强错误处理(中优先级)

  1. 在所有关键操作中添加 try-catch
  2. 失败时重新加载数据
  3. 确保界面与数据库一致

📝 控制台日志说明

正常流程日志

// 新项目
✅ [报价编辑器] 最终加载 3 个唯一空间
🔄 [报价编辑器] unifiedSpaces已同步: 3 → 3 个空间
📢 [报价编辑器] 已广播产品更新事件
🔔 [订单分配] 收到产品更新事件,重新加载数据...
✅ [订单分配] 数据重新加载完成

// 旧项目迁移
⚠️ [报价编辑器] 检测到旧项目,自动生成 unifiedSpaces...
✅ [报价编辑器] 旧项目数据已迁移
🔄 [报价编辑器] unifiedSpaces已同步: 0 → 5 个空间

// 重复数据清理
⚠️ 检测到 2 个重复产品,建议清理
🗑️ 开始清理 2 个重复产品...
  ✓ 已删除: 餐厅 (def456)
  ✓ 已删除: 主卧 (xyz789)
✅ 重复产品清理完成

🎊 总结

根本原因

  1. 订单分配阶段缺少事件监听 - 导致界面不更新
  2. 旧项目缺少 unifiedSpaces - 导致各阶段数据不一致
  3. Product表有重复记录 - 导致计算错误
  4. 同步时机和错误处理不完整 - 导致偶发性不同步

修复效果

修复后,所有项目(新旧项目)都将:

  • ✅ 实时同步数据更新
  • ✅ 各阶段显示一致
  • ✅ 自动清理重复数据
  • ✅ 容错性更强

修复工作已规划完毕,准备实施! 🚀