# 订单分配阶段 - 报价空间生成问题分析 ## 问题描述 用户点击"生成报价"按钮后,系统自动生成**9个默认空间**(客厅、餐厅、主卧、次卧、儿童房、书房、厨房、卫生间、阳台),而不是一开始默认1-2个或没有,让用户自己添加。 --- ## 问题根源定位 ### 🔍 核心问题:两个初始化路径冲突 系统中存在**两个不同的空间初始化路径**,导致行为不一致: #### ✅ 路径1:ProductSpaceService(正确) **文件**:`product-space.service.ts` 第674-780行 ```typescript async createInitialSpaces(projectId: string, projectType: '家装' | '工装' = '家装'): Promise { 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行 ```typescript // ❌ 预设场景列表 - 包含所有9个房间 presetScenes: { [key: string]: string[] } = { '家装': ['客厅', '餐厅', '主卧', '次卧', '儿童房', '书房', '厨房', '卫生间', '阳台'], // 9个! '工装': ['大堂', '接待区', '会议室', '办公区', '休息区', '展示区', '洽谈区'], // 7个! '建筑类': ['门头', '小型单体', '大型单体', '鸟瞰'] // 4个! }; // 第325-340行:创建默认产品 private async createDefaultProducts(): Promise { const defaultRooms = this.getDefaultRoomsForProjectType(); // 获取9个房间 for (const roomName of defaultRooms) { await this.createProduct(roomName); // ❌ 循环创建每个房间 } } ``` **触发条件**:(第310-311行) ```typescript 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 表(数据库) ```typescript { objectId: string; // 产品/空间ID project: Pointer; // 关联的项目 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; // 分配的设计师 // 进度信息 data: { progress: Array; // 进度数组 } } ``` ### 2️⃣ Project.data.quotation(项目数据) ```typescript { 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(统一空间存储 - 新增) ```typescript { 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行 ```typescript // ❌ 旧代码 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行 ```typescript // ❌ 旧代码 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行 ```typescript // ✅ 新代码:调用统一的初始化方法 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行附近) ```typescript import { ProductSpaceService } from '../services/product-space.service'; constructor( private cdr: ChangeDetectorRef, private productSpaceService: ProductSpaceService // 注入 ) {} ``` **优点**: - ✅ 复用已有的正确逻辑 - ✅ 保证两个路径的行为一致 - ✅ 只创建1-2个初始空间 - ✅ 代码维护性更好 **缺点**: - ⚠️ 需要注入ProductSpaceService依赖 --- ### 方案4:添加用户确认对话框(可选增强) **目标**:在自动创建前询问用户 **修改文件**:`quotation-editor.component.ts` 第310-316行 ```typescript 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) ```typescript presetScenes: { [key: string]: string[] } = { '家装': ['客厅', '主卧'], // 只保留2个 '工装': ['主要空间'], // 只保留1个 '建筑类': ['鸟瞰'] }; ``` 2. **修改 `createDefaultProducts` 调用统一方法**(方案3) ```typescript 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行) ```typescript private async saveQuotationToProject(): Promise { 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️⃣ 空间添加时 ```typescript 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️⃣ 空间删除时 ```typescript 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` 依赖 ### 可选增强 2. **quotation-editor.component.ts** - **第310-316行**:添加用户确认对话框(方案4) 3. **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个空间 - ✅ 各阶段空间数据保持一致