QUOTATION_EDITOR_SYNC_ANALYSIS.md 23 KB

报价空间管理器数据同步完整分析

🎯 核心问题

您报告的问题:删除订单分配阶段的空间后,其他阶段(需求确认、交付执行、设计师分配)仍显示9个空间,数据未同步。

📊 数据流分析

当前数据存储架构

┌─────────────────────────────────────────────────────────────┐
│                      数据存储层                               │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌─────────────────┐         ┌──────────────────┐           │
│  │  Product 表     │         │  Project 表      │           │
│  │  (空间产品)     │         │  (项目数据)      │           │
│  ├─────────────────┤         ├──────────────────┤           │
│  │ • productName   │         │ data.quotation   │           │
│  │ • productType   │         │   └─ spaces[]    │           │
│  │ • space {...}   │         │   └─ total       │           │
│  │ • quotation{..} │         │                  │           │
│  │ • requirements  │         │ data.unifiedSpaces[] ✅      │
│  │ • status        │         │   (统一空间数据)  │           │
│  │ • profile       │         │                  │           │
│  └─────────────────┘         └──────────────────┘           │
│                                                               │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                      业务逻辑层                               │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  QuotationEditorComponent (报价空间管理器)           │   │
│  ├──────────────────────────────────────────────────────┤   │
│  │  • loadProjectProducts()     - 从Product表加载       │   │
│  │  • createProduct()           - 创建Product记录       │   │
│  │  • deleteProduct()           - 删除Product记录       │   │
│  │  • generateQuotationFromProducts() - 生成报价        │   │
│  │  • saveQuotationToProject()  - 保存到Project.data   │   │
│  └──────────────────────────────────────────────────────┘   │
│                              ↓                                │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  ProductSpaceService (统一空间管理)                  │   │
│  ├──────────────────────────────────────────────────────┤   │
│  │  • saveUnifiedSpaceData()    - 保存统一空间数据 ✅   │   │
│  │  • getUnifiedSpaceData()     - 获取统一空间数据 ✅   │   │
│  │  • syncUnifiedSpacesToProducts() - 同步到Product ✅  │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                               │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                      展示层                                   │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  订单分配阶段 (stage-order.component.ts)                     │
│  ├─ 使用 QuotationEditorComponent                            │
│  ├─ 调用 ProductSpaceService.saveUnifiedSpaceData() ✅       │
│  └─ 删除空间后同步到统一存储 ✅                               │
│                                                               │
│  需求确认阶段 (stage-requirements.component.ts)              │
│  ├─ 调用 ProductSpaceService.getUnifiedSpaceData() ✅        │
│  └─ 从统一存储读取空间 ✅                                     │
│                                                               │
│  交付执行阶段 (stage-delivery.component.ts)                  │
│  ├─ 调用 ProductSpaceService.getUnifiedSpaceData() ✅        │
│  └─ 从统一存储读取空间 ✅                                     │
│                                                               │
│  设计师分配弹窗 (designer-team-assignment-modal.component)   │
│  ├─ 调用 ProductSpaceService.getUnifiedSpaceData() ✅        │
│  └─ 从统一存储读取空间 ✅                                     │
│                                                               │
└─────────────────────────────────────────────────────────────┘

🔍 QuotationEditorComponent 数据同步逻辑

1. 创建空间产品流程

// 用户点击"添加产品"
addProduct(productName)
    ↓
// 创建Product记录
createProduct(productName)
    ↓
    new Parse.Object('Product')
    product.set('productName', productName)
    product.set('space', { spaceName, area, spaceType, ... })
    product.set('quotation', { price, basePrice, ... })
    product.set('requirements', { ... })
    await product.save()  // ✅ 保存到Product表
    ↓
// 重新加载产品列表
loadProjectProducts()
    ↓
    const productQuery = new Parse.Query('Product')
    productQuery.equalTo('project', this.project.toPointer())
    this.products = await productQuery.find()  // ✅ 从Product表查询
    ↓
// 生成报价
generateQuotationFromProducts()
    ↓
    for (const product of this.products) {
      this.quotation.spaces.push({
        name: product.get('productName'),
        productId: product.id,  // ✅ 关联Product ID
        processes: {...},
        subtotal: ...
      })
    }
    ↓
// 保存报价到Project
saveQuotationToProject()
    ↓
    const data = this.project.get('data') || {}
    data.quotation = this.quotation  // ✅ 保存到Project.data.quotation
    this.project.set('data', data)
    await this.project.save()

❌ 问题saveQuotationToProject() 没有调用 ProductSpaceService.saveUnifiedSpaceData()


2. 删除空间产品流程

// 用户点击"删除产品"
deleteProduct(productId)
    ↓
    const product = this.products.find(p => p.id === productId)
    await product.destroy()  // ✅ 从Product表删除
    ↓
    // 更新本地产品列表
    this.products = this.products.filter(p => p.id !== productId)
    ↓
    // 更新报价spaces
    this.quotation.spaces = this.quotation.spaces.filter(
      (s: any) => s.productId !== productId
    )
    ↓
    // 重新计算总价
    this.calculateTotal()
    this.updateProductBreakdown()
    ↓
    // 保存报价
    await this.saveQuotationToProject()
        ↓
        const data = this.project.get('data') || {}
        data.quotation = this.quotation  // ✅ 保存到Project.data.quotation
        this.project.set('data', data)
        await this.project.save()

❌ 问题deleteProduct() 没有调用 ProductSpaceService.saveUnifiedSpaceData()


❌ 根本原因分析

问题1:QuotationEditorComponent 未使用统一空间管理

现状

  • QuotationEditorComponent 正确操作 Product表(创建、删除)
  • QuotationEditorComponent 正确保存 Project.data.quotation
  • QuotationEditorComponent 没有调用 ProductSpaceService.saveUnifiedSpaceData()
  • QuotationEditorComponent 没有更新 Project.data.unifiedSpaces

后果

订单分配阶段删除空间:
  ✅ Product表记录被删除
  ✅ Project.data.quotation.spaces 更新
  ❌ Project.data.unifiedSpaces 未更新 ← 这是关键问题!

其他阶段读取空间:
  ✅ 调用 ProductSpaceService.getUnifiedSpaceData()
  ❌ 读取到的是旧的 Project.data.unifiedSpaces(9个空间)
  ❌ 显示不一致!

问题2:数据同步链路断裂

QuotationEditorComponent
    ↓ (只保存到)
Project.data.quotation.spaces
    ↓ (没有同步到)
Project.data.unifiedSpaces  ← 断裂点!
    ↓ (其他阶段读取)
其他阶段显示旧数据

✅ 解决方案

方案1:在 QuotationEditorComponent 中集成统一空间管理

修改 saveQuotationToProject() 方法

// 📁 quotation-editor.component.ts

import { ProductSpaceService } from '../services/product-space.service';

export class QuotationEditorComponent {
  // 注入 ProductSpaceService
  constructor(
    private productSpaceService: ProductSpaceService
  ) {}

  /**
   * 保存报价到项目 - 集成统一空间管理
   */
  private async saveQuotationToProject(): Promise<void> {
    if (!this.project) return;

    try {
      console.log('💾 [报价管理器] 开始保存报价数据...');
      
      // 1️⃣ 保存到 Project.data.quotation(保持向后兼容)
      const data = this.project.get('data') || {};
      data.quotation = this.quotation;
      this.project.set('data', data);
      
      // 2️⃣ 🔥 关键:同步到统一空间管理
      const unifiedSpaces = this.products.map((product, index) => {
        const quotation = product.get('quotation') || {};
        const space = product.get('space') || {};
        const requirements = product.get('requirements') || {};
        const profile = product.get('profile');
        
        // 从报价数据中查找对应的空间信息
        const quotationSpace = this.quotation.spaces.find(
          (s: any) => s.productId === product.id
        );
        
        return {
          id: product.id,
          name: product.get('productName'),
          type: product.get('productType') || 'other',
          area: space.area || 0,
          priority: space.priority || 5,
          status: product.get('status') || 'not_started',
          complexity: space.complexity || 'medium',
          estimatedBudget: quotation.price || 0,
          order: index,
          // 报价信息
          quotation: {
            price: quotation.price || 0,
            processes: quotationSpace?.processes || {},
            subtotal: quotationSpace?.subtotal || 0
          },
          // 需求信息
          requirements: requirements,
          // 设计师分配
          designerId: profile?.id || null,
          // 进度信息
          progress: [],
          // 时间戳
          createdAt: product.createdAt?.toISOString() || new Date().toISOString(),
          updatedAt: new Date().toISOString()
        };
      });
      
      // 调用统一空间管理服务保存
      await this.productSpaceService.saveUnifiedSpaceData(
        this.project.id || '',
        unifiedSpaces
      );
      
      console.log(`✅ [报价管理器] 报价数据已保存,空间数: ${unifiedSpaces.length}`);
      
      this.quotationChange.emit(this.quotation);
    } catch (error) {
      console.error('❌ [报价管理器] 保存报价失败:', error);
      throw error;
    }
  }
}

修改 deleteProduct() 方法

/**
 * 删除产品 - 集成统一空间管理
 */
async deleteProduct(productId: string): Promise<void> {
  if (!await window?.fmode?.confirm('确定要删除这个产品吗?相关数据将被清除。')) return;

  try {
    console.log(`🗑️ [报价管理器] 开始删除产品: ${productId}`);
    
    // 1️⃣ 从Product表删除
    const product = this.products.find(p => p.id === productId);
    if (product) {
      await product.destroy();
      console.log(`  ✓ Product表记录已删除`);
    }

    // 2️⃣ 更新本地产品列表
    this.products = this.products.filter(p => p.id !== productId);
    this.productsChange.emit(this.products);

    // 3️⃣ 更新报价spaces
    this.quotation.spaces = this.quotation.spaces.filter(
      (s: any) => s.productId !== productId
    );

    // 4️⃣ 重新计算总价
    this.calculateTotal();
    this.updateProductBreakdown();

    // 5️⃣ 🔥 关键:保存到统一空间管理(会自动同步)
    await this.saveQuotationToProject();
    
    console.log(`✅ [报价管理器] 产品删除完成,剩余 ${this.products.length} 个产品`);
    
    window?.fmode?.alert('删除成功');

  } catch (error) {
    console.error('❌ [报价管理器] 删除产品失败:', error);
    window?.fmode?.alert('删除失败,请重试');
  }
}

方案2:在 stage-order.component.ts 中监听 QuotationEditorComponent 的变更

监听 productsChange 事件

// 📁 stage-order.component.html

<app-quotation-editor
  [project]="project"
  [canEdit]="canEdit"
  (productsChange)="onProductsChange($event)"
  (quotationChange)="onQuotationChange($event)"
></app-quotation-editor>
// 📁 stage-order.component.ts

/**
 * 监听报价管理器的产品变更
 */
async onProductsChange(products: any[]): Promise<void> {
  console.log('📦 [订单分配] 产品列表已更新:', products.length, '个产品');
  
  // 🔥 关键:同步到统一空间管理
  const unifiedSpaces = products.map((product, index) => {
    const quotation = product.get('quotation') || {};
    const space = product.get('space') || {};
    const requirements = product.get('requirements') || {};
    const profile = product.get('profile');
    
    return {
      id: product.id,
      name: product.get('productName'),
      type: product.get('productType') || 'other',
      area: space.area || 0,
      priority: space.priority || 5,
      status: product.get('status') || 'not_started',
      complexity: space.complexity || 'medium',
      estimatedBudget: quotation.price || 0,
      order: index,
      quotation: {
        price: quotation.price || 0,
        processes: {},
        subtotal: quotation.price || 0
      },
      requirements: requirements,
      designerId: profile?.id || null,
      progress: [],
      createdAt: product.createdAt?.toISOString() || new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };
  });
  
  await this.productSpaceService.saveUnifiedSpaceData(
    this.project!.id || '',
    unifiedSpaces
  );
  
  console.log(`✅ [订单分配] 统一空间数据已同步,空间数: ${unifiedSpaces.length}`);
}

/**
 * 监听报价管理器的报价变更
 */
async onQuotationChange(quotation: any): Promise<void> {
  console.log('💰 [订单分配] 报价数据已更新');
  this.quotation = quotation;
  
  // 更新项目数据
  const data = this.project!.get('data') || {};
  data.quotation = quotation;
  this.project!.set('data', data);
  
  // 注意:这里不需要再次调用 saveUnifiedSpaceData
  // 因为 QuotationEditorComponent 已经在 saveQuotationToProject 中调用了
}

📋 修改文件清单

必须修改的文件

  1. quotation-editor.component.ts ⭐ 核心修改

    • 注入 ProductSpaceService
    • 修改 saveQuotationToProject() 方法
    • 修改 deleteProduct() 方法
    • 修改 generateQuotationFromProducts() 方法
  2. stage-order.component.ts

    • 添加 onProductsChange() 方法
    • 添加 onQuotationChange() 方法
    • 在模板中绑定事件
  3. stage-order.component.html

    • 添加事件绑定:(productsChange)="onProductsChange($event)"
    • 添加事件绑定:(quotationChange)="onQuotationChange($event)"

🧪 验证步骤

测试场景1:添加空间产品

  1. 进入订单分配阶段
  2. 点击"添加产品",添加一个新空间(例如:"书房")
  3. 检查控制台日志:

    💾 [报价管理器] 开始保存报价数据...
    🔄 [统一空间管理] 开始保存统一空间数据,项目ID: xxx, 空间数: 6
    ✅ [统一空间管理] 统一空间数据保存完成
    ✅ [报价管理器] 报价数据已保存,空间数: 6
    
  4. 切换到需求确认阶段,验证显示6个空间 ✅

  5. 切换到交付执行阶段,验证显示6个空间 ✅

  6. 打开设计师分配弹窗,验证显示6个空间 ✅

测试场景2:删除空间产品

  1. 进入订单分配阶段
  2. 删除一个空间(例如:"书房")
  3. 检查控制台日志:

    🗑️ [报价管理器] 开始删除产品: xxx
     ✓ Product表记录已删除
    💾 [报价管理器] 开始保存报价数据...
    🔄 [统一空间管理] 开始保存统一空间数据,项目ID: xxx, 空间数: 5
    ✅ [统一空间管理] 统一空间数据保存完成
    ✅ [报价管理器] 产品删除完成,剩余 5 个产品
    
  4. 切换到需求确认阶段,验证显示5个空间 ✅

  5. 切换到交付执行阶段,验证显示5个空间 ✅

  6. 打开设计师分配弹窗,验证显示5个空间 ✅

测试场景3:刷新页面验证持久化

  1. 完成添加/删除操作后
  2. 刷新页面(F5)
  3. 进入各个阶段验证空间数量一致
  4. 检查数据库:

    const Parse = window.Parse || FmodeParse.with('nova');
    const query = new Parse.Query('Project');
    const project = await query.get('项目ID');
    const data = project.get('data');
       
    console.log('unifiedSpaces数量:', data.unifiedSpaces?.length);
    console.log('quotation.spaces数量:', data.quotation?.spaces?.length);
       
    const productQuery = new Parse.Query('Product');
    productQuery.equalTo('project', project.toPointer());
    const products = await productQuery.find();
    console.log('Product表数量:', products.length);
    

预期结果:三个数据源的数量完全一致 ✅


🎯 关键要点

1. 数据同步的黄金法则

任何对空间的增删改操作,必须同时更新三个地方:
  1️⃣ Product表(物理存储)
  2️⃣ Project.data.quotation.spaces(报价数据,向后兼容)
  3️⃣ Project.data.unifiedSpaces(统一空间数据,单一数据源) ⭐

2. 统一空间管理的核心价值

  • 单一数据源Project.data.unifiedSpaces 是所有阶段的唯一数据来源
  • 自动同步saveUnifiedSpaceData() 会自动同步到 Product表 和 quotation.spaces
  • 一致性保证:所有阶段读取同一数据源,确保显示一致

3. QuotationEditorComponent 的职责

  • ✅ 管理 Product表的 CRUD 操作
  • ✅ 生成报价数据(quotation.spaces
  • 新增:调用 ProductSpaceService.saveUnifiedSpaceData() 同步数据

📊 数据一致性检查清单

操作后必须验证的数据

数据源 字段路径 验证方法
Product表 Product.productName Parse.Query('Product').find()
Project.data data.quotation.spaces[] project.get('data').quotation.spaces
Project.data data.unifiedSpaces[] project.get('data').unifiedSpaces

一致性验证脚本

// 在浏览器控制台执行
async function checkSpaceConsistency(projectId) {
  const Parse = window.Parse || FmodeParse.with('nova');
  
  // 1. 查询Project
  const projectQuery = new Parse.Query('Project');
  const project = await projectQuery.get(projectId);
  const data = project.get('data') || {};
  
  // 2. 查询Product表
  const productQuery = new Parse.Query('Product');
  productQuery.equalTo('project', project.toPointer());
  productQuery.notEqualTo('isDeleted', true);
  const products = await productQuery.find();
  
  // 3. 对比数量
  const unifiedCount = data.unifiedSpaces?.length || 0;
  const quotationCount = data.quotation?.spaces?.length || 0;
  const productCount = products.length;
  
  console.log('📊 空间数量一致性检查:');
  console.log(`  unifiedSpaces: ${unifiedCount}`);
  console.log(`  quotation.spaces: ${quotationCount}`);
  console.log(`  Product表: ${productCount}`);
  
  if (unifiedCount === quotationCount && quotationCount === productCount) {
    console.log('✅ 数据一致!');
    return true;
  } else {
    console.error('❌ 数据不一致!需要修复。');
    return false;
  }
}

// 使用方法
await checkSpaceConsistency('你的项目ID');

🚀 实施步骤

第1步:修改 QuotationEditorComponent

  1. 注入 ProductSpaceService
  2. 修改 saveQuotationToProject() 方法(添加统一空间同步)
  3. 确保 deleteProduct() 调用 saveQuotationToProject()

第2步:修改 stage-order.component

  1. 添加 onProductsChange() 事件处理
  2. 在模板中绑定事件

第3步:测试验证

  1. 清除浏览器缓存
  2. 刷新页面
  3. 执行测试场景1、2、3
  4. 运行一致性检查脚本

第4步:数据修复(如果需要)

如果现有项目的数据不一致,运行修复脚本:

// 修复单个项目
await productSpaceService.forceRepairSpaceData('项目ID');

// 或在浏览器控制台执行
const service = // 获取ProductSpaceService实例
await service.forceRepairSpaceData('项目ID');

✅ 预期结果

修改完成后:

  1. ✅ 订单分配阶段添加/删除空间 → 立即同步到统一存储
  2. ✅ 需求确认阶段 → 显示正确的空间数量
  3. ✅ 交付执行阶段 → 显示正确的空间数量
  4. ✅ 设计师分配弹窗 → 显示正确的空间数量
  5. ✅ 刷新页面后 → 所有阶段数据保持一致
  6. ✅ 数据库三个数据源 → 数量完全一致

修复完成时间: ___________
测试人员: ___________
测试结果: ✅ 通过 / ❌ 未通过