ORDER_QUOTATION_SPACE_ANALYSIS.md 18 KB

订单分配阶段 - 报价空间生成问题分析

问题描述

用户点击"生成报价"按钮后,系统自动生成9个默认空间(客厅、餐厅、主卧、次卧、儿童房、书房、厨房、卫生间、阳台),而不是一开始默认1-2个或没有,让用户自己添加。


问题根源定位

🔍 核心问题:两个初始化路径冲突

系统中存在两个不同的空间初始化路径,导致行为不一致:

✅ 路径1:ProductSpaceService(正确)

文件product-space.service.ts 第674-780行

async createInitialSpaces(projectId: string, projectType: '家装' | '工装' = '家装'): Promise<any[]> {
  if (projectType === '家装') {
    // ✅ 只创建2个初始空间:客厅 + 主卧
    initialSpaces = [
      { name: '客厅', type: 'living_room', ... },
      { name: '主卧', type: 'bedroom', ... }
    ];
  } else {
    // ✅ 工装只创建1个主要空间
    initialSpaces = [
      { name: '主要空间', type: 'other', ... }
    ];
  }
}

特点

  • ✅ 家装项目只创建2个空间(客厅 + 主卧)
  • ✅ 工装项目只创建1个空间(主要空间)
  • ✅ 用户可以后续自己添加更多空间

❌ 路径2:QuotationEditor(问题来源)

文件quotation-editor.component.ts 第103-105行 + 第325-340行

// ❌ 预设场景列表 - 包含所有9个房间
presetScenes: { [key: string]: string[] } = {
  '家装': ['客厅', '餐厅', '主卧', '次卧', '儿童房', '书房', '厨房', '卫生间', '阳台'],  // 9个!
  '工装': ['大堂', '接待区', '会议室', '办公区', '休息区', '展示区', '洽谈区'],      // 7个!
  '建筑类': ['门头', '小型单体', '大型单体', '鸟瞰']                               // 4个!
};

// 第325-340行:创建默认产品
private async createDefaultProducts(): Promise<void> {
  const defaultRooms = this.getDefaultRoomsForProjectType();  // 获取9个房间
  
  for (const roomName of defaultRooms) {
    await this.createProduct(roomName);  // ❌ 循环创建每个房间
  }
}

触发条件:(第310-311行)

if (this.products.length === 0) {
  await this.createDefaultProducts();  // ❌ Product表为空时自动创建9个空间
}

特点

  • ❌ 家装项目一次性创建9个空间
  • ❌ 工装项目一次性创建7个空间
  • ❌ 不给用户选择的机会,强制创建所有空间

数据流程分析

订单分配阶段的数据流程

订单分配页面加载
    ↓
stage-order.loadProjectSpaces() (第704行)
    ↓
ProductSpaceService.getUnifiedSpaceData() (第712行)
    ↓
【分支1】如果有统一空间数据
    → 直接加载 ✅
    ↓
【分支2】如果没有,尝试从Product表加载 (第738行)
    → ProductSpaceService.getProjectProductSpaces()
    ↓
【分支3】如果Product表也没有
    ↓
    3.1 如果有报价数据
        → 从报价数据创建空间 (第745行)
        → createSpacesFromQuotation()
    ↓
    3.2 如果没有报价数据 (第748-752行)
        → ✅ 调用 ProductSpaceService.createInitialSpaces()
        → 只创建1-2个初始空间

报价编辑器的数据流程

QuotationEditor组件初始化
    ↓
loadProjectProducts() (第272行)
    ↓
查询Product表
    ↓
【分支1】如果Product表有数据
    → 加载并去重 ✅
    ↓
【分支2】如果Product表为空 (第310-311行)
    → ❌ 调用 createDefaultProducts()
    → ❌ 根据 presetScenes 创建9个空间
    → ❌ 循环创建Product记录

问题触发路径

用户进入订单分配阶段
    ↓
没有空间数据
    ↓
stage-order调用createInitialSpaces() (✅ 创建2个空间)
    ↓
用户点击"生成报价"
    ↓
QuotationEditor.generateQuotationFromProducts() (第541行)
    ↓
loadProjectProducts() 检测到Product表有数据 (✅ 只有2个空间)
    ↓
正常生成2个空间的报价 ✅

但如果用户直接点击"生成报价"而没有先保存空间:

用户进入订单分配阶段
    ↓
用户直接点击"生成报价"(没有先保存空间)
    ↓
QuotationEditor.loadProjectProducts()
    ↓
Product表为空
    ↓
❌ 调用 createDefaultProducts()
    ↓
❌ 从 presetScenes['家装'] 获取9个房间
    ↓
❌ 循环创建9个Product记录
    ↓
❌ 生成9个空间的报价

数据存储字段

1️⃣ Product 表(数据库)

{
  objectId: string;              // 产品/空间ID
  project: Pointer<Project>;     // 关联的项目
  productName: string;           // 空间名称,例如:"客厅"
  productType: string;           // 空间类型,例如:"living_room"
  status: string;                // 空间状态:"not_started" | "in_progress" | "completed"
  
  // 空间信息
  space: {
    spaceName: string;           // 空间名称(冗余)
    area: number;                // 面积(平方米)
    priority: number;            // 优先级(1-10)
    complexity: string;          // 复杂度:"low" | "medium" | "high"
    styleLevel: string;          // 风格等级
    businessType: string;        // 业务类型
    architectureType: string;    // 建筑类型
  },
  
  // 报价信息
  quotation: {
    price: number;               // 单价
    currency: string;            // 货币单位:"CNY"
    breakdown: object;           // 价格明细
    status: string;              // 报价状态
    processes: object;           // 工序分配
    subtotal: number;            // 小计
  },
  
  // 需求信息
  requirements: object;          // 空间需求
  
  // 设计师分配
  profile: Pointer<Profile>;     // 分配的设计师
  
  // 进度信息
  data: {
    progress: Array;             // 进度数组
  }
}

2️⃣ Project.data.quotation(项目数据)

{
  spaces: [
    {
      name: string;              // 空间名称,例如:"客厅"
      spaceId: string;           // 关联的Product ID
      productId: string;         // 同上(别名)
      
      // 工序分配(新版:按公司分配方式)
      processes: {
        modeling: {              // 建模阶段(10%)
          enabled: boolean,
          amount: number,
          percentage: number,
          description: string
        },
        decoration: {            // 软装渲染阶段(40%)
          enabled: boolean,
          amount: number,
          percentage: number,
          description: string
        },
        company: {               // 公司运营与利润(50%)
          enabled: boolean,
          amount: number,
          percentage: number,
          description: string
        }
      },
      
      subtotal: number           // 空间小计
    }
  ],
  total: number,                 // 报价总额
  spaceBreakdown: [              // 空间占比明细
    {
      spaceName: string,
      spaceId: string,
      amount: number,
      percentage: number
    }
  ],
  generatedAt: Date,             // 生成时间
  validUntil: Date               // 有效期至
}

3️⃣ Project.data.unifiedSpaces(统一空间存储 - 新增)

{
  unifiedSpaces: [
    {
      id: string,                // 空间ID(对应Product.objectId)
      name: string,              // 空间名称
      type: string,              // 空间类型
      area: number,              // 面积
      priority: number,          // 优先级
      status: string,            // 状态
      complexity: string,        // 复杂度
      estimatedBudget: number,   // 预估预算
      order: number,             // 排序
      
      quotation: {               // 报价信息(与quotation.spaces同步)
        price: number,
        processes: object,
        subtotal: number
      },
      
      requirements: object,      // 需求信息
      designerId: string | null, // 分配的设计师ID
      progress: Array,           // 进度
      createdAt: string,         // 创建时间
      updatedAt: string          // 更新时间
    }
  ]
}

修复方案

方案1:修改预设场景为空(推荐)

目标:让用户手动添加空间,而不是自动生成

修改文件quotation-editor.component.ts 第103-107行

// ❌ 旧代码
presetScenes: { [key: string]: string[] } = {
  '家装': ['客厅', '餐厅', '主卧', '次卧', '儿童房', '书房', '厨房', '卫生间', '阳台'],
  '工装': ['大堂', '接待区', '会议室', '办公区', '休息区', '展示区', '洽谈区'],
  '建筑类': ['门头', '小型单体', '大型单体', '鸟瞰']
};

// ✅ 新代码:改为空数组或只保留1-2个默认空间
presetScenes: { [key: string]: string[] } = {
  '家装': [],           // 不自动创建,让用户添加
  '工装': [],
  '建筑类': []
};

// 或者只保留1-2个默认空间
presetScenes: { [key: string]: string[] } = {
  '家装': ['客厅', '主卧'],  // 只创建2个初始空间
  '工装': ['主要空间'],       // 只创建1个初始空间
  '建筑类': ['鸟瞰']
};

优点

  • ✅ 简单直接,只需修改一处
  • ✅ 让用户主动添加需要的空间
  • ✅ 避免浪费(不需要的空间不会被创建)

缺点

  • ⚠️ 新用户可能不知道如何添加空间(需要UI引导)

方案2:禁用自动创建默认产品(推荐)

目标:不自动创建Product记录,依赖订单分配阶段的初始化

修改文件quotation-editor.component.ts 第310-316行

// ❌ 旧代码
if (this.products.length === 0) {
  await this.createDefaultProducts();  // 自动创建9个空间
} else {
  await this.loadProductCollaborations();
}

// ✅ 新代码:不自动创建,提示用户添加
if (this.products.length === 0) {
  console.warn('⚠️ 没有找到空间数据,请先在订单分配阶段创建空间');
  // 不自动创建,等待用户手动添加
  // await this.createDefaultProducts();  // ❌ 注释掉
} else {
  await this.loadProductCollaborations();
}

优点

  • ✅ 彻底禁用自动创建行为
  • ✅ 与订单分配阶段的初始化逻辑保持一致
  • ✅ 用户体验更可控

缺点

  • ⚠️ 需要确保订单分配阶段已正确初始化空间
  • ⚠️ 需要提示用户如何添加空间

方案3:调用统一初始化方法(最佳)

目标:复用 ProductSpaceService.createInitialSpaces() 的逻辑

修改文件quotation-editor.component.ts 第310-316行

// ✅ 新代码:调用统一的初始化方法
if (this.products.length === 0) {
  console.log('🏠 没有找到空间数据,创建初始空间...');
  
  // 调用ProductSpaceService的初始化方法(只创建1-2个空间)
  const initialSpaces = await this.productSpaceService.createInitialSpaces(
    this.project!.id || '',
    this.projectInfo.projectType as '家装' | '工装'
  );
  
  console.log(`✅ 已创建 ${initialSpaces.length} 个初始空间`);
  
  // 重新加载产品列表
  await this.loadProjectProducts();
} else {
  await this.loadProductCollaborations();
}

需要注入依赖:(第1行附近)

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

constructor(
  private cdr: ChangeDetectorRef,
  private productSpaceService: ProductSpaceService  // 注入
) {}

优点

  • ✅ 复用已有的正确逻辑
  • ✅ 保证两个路径的行为一致
  • ✅ 只创建1-2个初始空间
  • ✅ 代码维护性更好

缺点

  • ⚠️ 需要注入ProductSpaceService依赖

方案4:添加用户确认对话框(可选增强)

目标:在自动创建前询问用户

修改文件quotation-editor.component.ts 第310-316行

if (this.products.length === 0) {
  // 询问用户是否要创建默认空间
  const confirmed = await window?.fmode?.confirm(
    '检测到没有空间数据,是否创建默认空间(客厅+主卧)?\n' +
    '您也可以点击"取消"后手动添加空间。'
  );
  
  if (confirmed) {
    // 调用统一的初始化方法(只创建2个空间)
    await this.productSpaceService.createInitialSpaces(
      this.project!.id || '',
      this.projectInfo.projectType as '家装' | '工装'
    );
    await this.loadProjectProducts();
  } else {
    console.log('用户取消了创建默认空间,等待手动添加');
  }
} else {
  await this.loadProductCollaborations();
}

优点

  • ✅ 用户体验最好,给用户选择权
  • ✅ 避免强制行为
  • ✅ 适合新手和老手用户

推荐方案组合

最佳实践:方案3 + 方案1

  1. 修改 presetScenes 为少量默认值(方案1)

    presetScenes: { [key: string]: string[] } = {
    '家装': ['客厅', '主卧'],   // 只保留2个
    '工装': ['主要空间'],        // 只保留1个
    '建筑类': ['鸟瞰']
    };
    
  2. 修改 createDefaultProducts 调用统一方法(方案3)

    if (this.products.length === 0) {
    // 调用统一的初始化方法
    await this.productSpaceService.createInitialSpaces(
    this.project!.id || '',
    this.projectInfo.projectType as '家装' | '工装'
    );
    await this.loadProjectProducts();
    }
    
  3. 可选:添加用户确认对话框(方案4)

效果

  • ✅ 新项目只创建1-2个初始空间
  • ✅ 两个初始化路径行为一致
  • ✅ 用户可以自己添加更多空间
  • ✅ 不会一次性生成9个空间

数据同步机制

当前同步流程

用户在报价编辑器中修改空间
    ↓
QuotationEditor.generateQuotationFromProducts() (第541行)
    ↓
生成 quotation.spaces 数据
    ↓
QuotationEditor.saveQuotationToProject() (第664行)
    ↓
【同步1】保存到 Project.data.quotation ✅
    ↓
【同步2】保存到 Project.data.unifiedSpaces ✅ (第672-702行)
    ↓
【同步3】同步到 Product 表 (需要调用 syncUnifiedSpacesToProducts)

关键同步点

1️⃣ 报价保存时(第664-713行)

private async saveQuotationToProject(): Promise<void> {
  const data = this.project.get('data') || {};
  
  // 保存报价数据
  data.quotation = this.quotation;
  
  // 🔥 同步到统一空间存储
  data.unifiedSpaces = this.products.map((product, index) => {
    const quotationSpace = this.quotation.spaces.find(s => s.productId === product.id);
    return {
      id: product.id,
      name: product.get('productName'),
      quotation: {
        price: quotation.price,
        processes: quotationSpace?.processes || {},
        subtotal: quotationSpace?.subtotal || 0
      },
      // ... 其他字段
    };
  });
  
  await this.project.save();
  this.quotationChange.emit(this.quotation);
}

2️⃣ 空间添加时

async createProduct(productName: string) {
  // 创建Product记录
  const Product = Parse.Object.extend('Product');
  const product = new Product();
  product.set('productName', productName);
  product.set('project', this.project.toPointer());
  // ...设置其他字段
  await product.save();
  
  // 重新加载产品列表
  await this.loadProjectProducts();
  
  // 🔥 需要同步到 unifiedSpaces
  // TODO: 调用 saveQuotationToProject() 或 ProductSpaceService.saveUnifiedSpaceData()
}

3️⃣ 空间删除时

async deleteProduct(productId: string) {
  // 删除Product记录
  const product = this.products.find(p => p.id === productId);
  await product.destroy();
  
  // 重新加载产品列表
  await this.loadProjectProducts();
  
  // 🔥 需要同步到 unifiedSpaces
  // TODO: 调用 saveQuotationToProject()
}

验证步骤

修复前验证

  1. 创建新项目
  2. 进入订单分配阶段
  3. 点击"生成报价"按钮
  4. 检查控制台日志

    🔍 [报价编辑器] Product表查询结果: 0 条记录
    创建默认产品...
    ✅ 已创建产品: 客厅
    ✅ 已创建产品: 餐厅
    ✅ 已创建产品: 主卧
    ✅ 已创建产品: 次卧
    ✅ 已创建产品: 儿童房
    ✅ 已创建产品: 书房
    ✅ 已创建产品: 厨房
    ✅ 已创建产品: 卫生间
    ✅ 已创建产品: 阳台
    🔍 [报价编辑器] Product表查询结果: 9 条记录  ← ❌ 问题!
    
  5. 检查报价明细:显示9个空间

修复后验证

  1. 创建新项目
  2. 进入订单分配阶段
  3. 自动初始化

    🏠 [初始空间] 为项目 abc123 创建初始空间,类型: 家装
    ✅ [初始空间] 已创建 2 个初始空间  ← ✅ 正确!
    
  4. 点击"生成报价"按钮

  5. 检查控制台日志

    🔍 [报价编辑器] Product表查询结果: 2 条记录  ← ✅ 正确!
    ✅ 报价空间生成完成: 2 个唯一空间 (原始产品: 2 个)
    
  6. 检查报价明细:只显示2个空间(客厅 + 主卧)

  7. 用户手动添加更多空间:点击"添加空间"按钮


文件修改清单

必须修改

  1. quotation-editor.component.ts
    • 第103-107行:修改 presetScenes 为少量默认值(方案1)
    • 第310-316行:调用统一初始化方法(方案3)
    • 第1行附近:注入 ProductSpaceService 依赖

可选增强

  1. quotation-editor.component.ts

    • 第310-316行:添加用户确认对话框(方案4)
  2. quotation-editor.component.html

    • 添加"添加空间"按钮的引导提示
    • 空状态时显示友好提示

总结

问题根源

  • QuotationEditor 组件的 presetScenes 包含9个默认房间
  • 当Product表为空时,自动创建所有预设房间的Product记录
  • ProductSpaceService 的初始化逻辑(只创建1-2个空间)不一致

解决方案

  1. 修改 presetScenes 为少量默认值(1-2个)
  2. 修改 createDefaultProducts 调用统一的 createInitialSpaces 方法
  3. 可选:添加用户确认对话框

数据存储

  • Product表:存储空间基础信息
  • Project.data.quotation.spaces:存储报价明细
  • Project.data.unifiedSpaces:统一空间数据(同步到各阶段)

预期效果

  • ✅ 新项目只创建1-2个初始空间
  • ✅ 用户可以手动添加更多空间
  • ✅ 不会一次性生成9个空间
  • ✅ 各阶段空间数据保持一致