# 项目空间数量同步分析报告 - 第一部分 ## 📋 执行摘要 本报告分析了订单分配阶段、报价明细、交付执行板块以及设计师分配弹窗中的**空间数量同步机制**。 ### 核心发现: - ✅ **空间数据存储**:使用 `Product` 表(对应 `ProductSpace` 服务) - ✅ **同步机制**:通过 `ProductSpaceService.getProjectProductSpaces()` 统一查询 - ✅ **数据字段**:Project.data 中存储报价信息,Product 表存储空间产品信息 - ⚠️ **同步状态**:**部分同步**,存在潜在的数据不一致风险 --- ## 1. 空间数据存储字段 ### 1.1 Product 表(ProductSpace) **表名**:`Product`(通过 `ProductSpaceService` 访问) **核心字段**: ```typescript interface ProductSpace { id: string; // 产品ID name: string; // 空间名称(如"客厅"、"卧室") type: string; // 空间类型(living_room, bedroom, kitchen等) area?: number; // 面积(㎡) priority?: number; // 优先级 status: string; // 状态(not_started, in_progress, completed等) complexity?: string; // 复杂度(low, medium, high) estimatedBudget?: number; // 预算 order?: number; // 排序顺序 metadata?: { description?: string; // 描述 }; project: Pointer; // 关联的项目指针 company: Pointer; // 关联的公司指针 } ``` **存储位置**:Parse Server 的 `Product` 表 **查询方法**: ```typescript // ProductSpaceService async getProjectProductSpaces(projectId: string): Promise ``` ### 1.2 Project 表(项目数据) **表名**:`Project` **空间相关字段**: ```typescript interface ProjectData { // 报价信息(存储在 project.data.quotation) quotation?: { spaces: Array<{ name: string; // 空间名称 spaceId?: string; // 关联的Product ID processes: { modeling: { enabled, price, quantity }; softDecor: { enabled, price, quantity }; rendering: { enabled, price, quantity }; postProcess: { enabled, price, quantity }; }; subtotal: number; // 小计 }>; total: number; // 总价 spaceBreakdown: Array<{ spaceName: string; spaceId: string; amount: number; percentage: number; }>; }; // 特殊需求(存储在 project.data.spaceSpecialRequirements) spaceSpecialRequirements?: { [spaceId: string]: { content: string; updatedAt: Date; updatedBy: string; }; }; } ``` **存储位置**:Parse Server 的 `Project` 表的 `data` 字段(JSON格式) --- ## 2. 空间数量同步流程 ### 2.1 订单分配阶段(stage-order.component.ts) **文件**:`src/modules/project/pages/project-detail/stages/stage-order.component.ts` **空间加载流程**: ```typescript // 第695-758行:loadProjectSpaces() async loadProjectSpaces(): Promise { // 1. 从ProductSpace表加载空间数据 this.projectSpaces = await this.productSpaceService.getProjectProductSpaces( this.project.id ); // 2. 如果没有空间数据,但从项目数据中有报价信息,则转换创建默认空间 if (this.projectSpaces.length === 0) { const data = this.project.get('data') || {}; if (data.quotation?.spaces) { await this.createSpacesFromQuotation(data.quotation.spaces); } } // 3. ⭐ 同步缺失空间:当已有部分 ProductSpace 时,补齐与报价明细一致 if (this.projectSpaces.length > 0) { const quotationSpaces = Array.isArray(data.quotation?.spaces) ? data.quotation.spaces : []; // 找出报价中有但Product表中没有的空间 const missing = quotationSpaces.filter((s: any) => { const n = (s?.name || '').trim().toLowerCase(); return n && !existingNames.has(n); }); // 为缺失的空间创建Product记录 if (missing.length > 0) { for (const spaceData of missing) { await this.productSpaceService.createProductSpace( this.project!.id, { name: spaceData.name, type: this.inferSpaceType(spaceData.name), status: 'not_started', complexity: 'medium', order: this.projectSpaces.length } ); } // 重新加载补齐后的空间列表 this.projectSpaces = await this.productSpaceService .getProjectProductSpaces(this.project.id); } } } ``` **关键方法**: | 方法名 | 功能 | 行号 | |--------|------|------| | `loadProjectSpaces()` | 加载项目空间 | 695-758 | | `createSpacesFromQuotation()` | 从报价创建空间 | 763-784 | | `inferSpaceType()` | 推断空间类型 | 789-797 | | `calculateSpaceRate()` | 计算空间预算 | 802-811 | | `regenerateQuotationFromSpaces()` | 从空间重新生成报价 | 950-996 | | `updateSpaceBreakdown()` | 更新空间占比 | 1001-1008 | **数据同步逻辑**: 1. 优先从 Product 表加载空间 2. 如果 Product 表为空,从 Project.data.quotation 创建 3. 如果两者都有,检查缺失的空间并自动创建 ### 2.2 报价明细(quotation-editor.component.ts) **文件**:`src/modules/project/components/quotation-editor.component.ts` **空间数量来源**: ```typescript // 从项目的Product表获取空间数量 this.projectSpaces = await this.productSpaceService .getProjectProductSpaces(this.projectId); // 空间数量 = this.projectSpaces.length const spaceCount = this.projectSpaces.length; ``` **报价空间结构**: ```typescript quotation.spaces: Array<{ name: string; // 空间名称 spaceId?: string; // 关联的Product ID processes: {...}; // 工序明细 subtotal: number; // 小计 }> // 空间数量 = quotation.spaces.length ``` **同步机制**: - 报价中的 `spaceId` 字段关联 Product 表的 ID - 通过 `spaceId` 建立报价与Product的关联 ### 2.3 交付执行板块(stage-delivery.component.ts) **文件**:`src/modules/project/pages/project-detail/stages/stage-delivery.component.ts` **空间加载流程**: ```typescript // 第145-188行:syncProductsWithQuotation() private async syncProductsWithQuotation(): Promise { if (!this.project) return; const data = this.project.get('data') || {}; const quotationSpaces: any[] = Array.isArray(data.quotation?.spaces) ? data.quotation.spaces : []; if (quotationSpaces.length === 0) return; // 检查哪些空间在Product表中缺失 const existingById = new Set(this.projectProducts.map(p => p.id)); const existingByName = new Set( this.projectProducts.map(p => (p.name || '').trim().toLowerCase()) ); const missing = quotationSpaces.filter(s => { const n = (s?.name || '').trim().toLowerCase(); const pid = s?.productId || ''; if (pid && existingById.has(pid)) return false; if (n && existingByName.has(n)) return false; return true; }); // 为缺失的空间创建Product记录 if (missing.length > 0) { for (const s of missing) { await this.productSpaceService.createProductSpace( this.project!.id, { name: s?.name || '未命名空间', type: 'other', priority: 5, status: 'not_started', complexity: 'medium', order: this.projectProducts.length } ); } // 重新加载并去重 this.projectProducts = await this.productSpaceService .getProjectProductSpaces(this.project.id); this.projectProducts = this.projectProducts.filter((p, idx, arr) => { const key = (p.name || '').trim().toLowerCase(); return arr.findIndex(x => (x.name || '').trim().toLowerCase() === key) === idx; }); } } ``` **关键特性**: - 自动去重(按名称) - 同步报价中的空间到Product表 - 支持按ID或名称匹配 ### 2.4 设计师分配弹窗(designer-team-assignment-modal.component.ts) **文件**:`src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts` **空间加载流程**: ```typescript // 第84-88行:输入属性 @Input() spaceScenes: SpaceScene[] = []; // 空间场景列表 @Input() enableSpaceAssignment: boolean = false; // 是否启用空间分配 @Input() projectId: string = ''; // 项目ID @Input() loadRealData: boolean = true; // 是否加载真实数据 @Input() loadRealSpaces: boolean = true; // 是否自动加载真实空间数据 // 第296-298行:ngOnInit() if (this.loadRealSpaces && this.projectId) { await this.loadRealProjectSpaces(); } // 第951-987行:loadRealProjectSpaces() async loadRealProjectSpaces() { if (!this.projectId) { console.warn('未提供projectId,无法加载空间数据'); return; } try { this.loadingSpaces = true; this.spaceLoadError = ''; // 使用ProductSpaceService查询项目的所有空间产品 this.parseProducts = await this.productSpaceService .getProjectProductSpaces(this.projectId); if (this.parseProducts.length === 0) { console.warn('未找到项目空间数据'); this.spaceLoadError = '未找到项目空间数据'; return; } // 转换为SpaceScene格式 this.spaceScenes = this.parseProducts.map(product => ({ id: product.id, name: product.name, area: product.area, description: this.getProductDescription(product) })); console.log('成功加载项目空间数据:', this.spaceScenes); } catch (err) { console.error('加载项目空间数据失败:', err); this.spaceLoadError = '加载项目空间数据失败'; } finally { this.loadingSpaces = false; this.cdr.markForCheck(); } } ``` **空间分配数据结构**: ```typescript // 第53-57行:DesignerSpaceAssignment export interface DesignerSpaceAssignment { designerId: string; designerName: string; spaceIds: string[]; // 分配的空间ID列表 } // 第1311-1315行:getDesignerSpacesText() getDesignerSpacesText(designerId: string): string { const spaces = this.getDesignerSpaces(designerId); if (spaces.length === 0) return '未分配空间'; return spaces.map(s => s.name).join(', '); } // 第1305-1308行:getDesignerSpaces() getDesignerSpaces(designerId: string): SpaceScene[] { const spaceIds = this.designerSpaceMap.get(designerId) || []; return this.spaceScenes.filter(space => spaceIds.includes(space.id)); } ``` **关键方法**: | 方法名 | 功能 | 行号 | |--------|------|------| | `loadRealProjectSpaces()` | 加载真实空间数据 | 951-987 | | `getDesignerSpaces()` | 获取设计师分配的空间 | 1305-1308 | | `getDesignerSpacesText()` | 获取空间名称文本 | 1311-1315 | | `isSpaceSelected()` | 检查空间是否被选中 | 1299-1302 | | `toggleSpaceSelection()` | 切换空间选择 | (需查看完整代码) | --- ## 3. 数据同步现状分析 ### 3.1 同步机制总结 ``` 订单分配阶段 (stage-order) ↓ ├─ 从Product表加载空间 │ └─ productSpaceService.getProjectProductSpaces() │ ├─ 从Project.data.quotation同步缺失空间 │ └─ 自动创建Product记录 │ └─ 生成报价 (quotation.spaces) └─ 每个空间关联spaceId 报价明细 (quotation-editor) ↓ ├─ 读取quotation.spaces │ └─ 空间数量 = quotation.spaces.length │ └─ 通过spaceId关联Product表 交付执行 (stage-delivery) ↓ ├─ 从Product表加载空间 │ └─ productSpaceService.getProjectProductSpaces() │ ├─ 同步报价中的缺失空间 │ └─ 自动创建Product记录 │ └─ 去重处理 └─ 按名称去重 设计师分配 (designer-team-assignment-modal) ↓ ├─ 从Product表加载空间 │ └─ productSpaceService.getProjectProductSpaces() │ ├─ 转换为SpaceScene格式 │ └─ { id, name, area, description } │ └─ 分配空间给设计师 └─ designerSpaceMap: Map ``` ### 3.2 同步数据字段对应关系 | 阶段 | 数据来源 | 字段 | 空间数量 | 同步方式 | |------|---------|------|---------|---------| | 订单分配 | Product表 | `projectSpaces[]` | `length` | 自动创建缺失空间 | | 报价明细 | Project.data | `quotation.spaces[]` | `length` | 通过spaceId关联 | | 交付执行 | Product表 | `projectProducts[]` | `length` | 自动创建缺失空间 | | 设计师分配 | Product表 | `spaceScenes[]` | `length` | 直接查询Product | ### 3.3 同步一致性检查 **✅ 已实现的同步**: 1. **Product表 ↔ 报价明细**:通过 `spaceId` 字段关联 2. **Product表 ↔ 交付执行**:通过 `syncProductsWithQuotation()` 自动同步 3. **Product表 ↔ 设计师分配**:通过 `loadRealProjectSpaces()` 加载 **⚠️ 潜在的不一致风险**: 1. **报价修改后未同步Product表** - 问题:在报价编辑器中添加/删除空间,不会自动更新Product表 - 影响:交付执行和设计师分配看到的空间数量可能不一致 - 解决方案:需要在报价保存时同步Product表 2. **Product表删除后未更新报价** - 问题:直接删除Product记录,报价中仍保留该空间 - 影响:报价中的spaceId可能指向不存在的Product - 解决方案:需要级联删除或软删除处理 3. **空间名称修改后的同步** - 问题:修改Product的name,报价中的name可能不同步 - 影响:显示的空间名称可能不一致 - 解决方案:需要定期同步或使用spaceId作为唯一标识 --- ## 4. 关键代码文件清单 | 文件路径 | 功能 | 关键方法 | |---------|------|---------| | `src/modules/project/services/product-space.service.ts` | 空间产品服务 | `getProjectProductSpaces()`, `createProductSpace()`, `updateProductSpace()`, `deleteProductSpace()` | | `src/modules/project/pages/project-detail/stages/stage-order.component.ts` | 订单分配阶段 | `loadProjectSpaces()`, `createSpacesFromQuotation()`, `regenerateQuotationFromSpaces()` | | `src/modules/project/components/quotation-editor.component.ts` | 报价编辑器 | (需查看完整代码) | | `src/modules/project/pages/project-detail/stages/stage-delivery.component.ts` | 交付执行阶段 | `syncProductsWithQuotation()` | | `src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts` | 设计师分配弹窗 | `loadRealProjectSpaces()`, `getDesignerSpaces()`, `getDesignerSpacesText()` | --- ## 5. 总结 ### 核心同步机制: 1. **Product表** 是空间数据的唯一真实来源 2. **Project.data.quotation** 存储报价信息,包含空间明细 3. 各阶段通过 `ProductSpaceService.getProjectProductSpaces()` 统一查询空间 4. 自动同步机制确保Product表与报价数据基本一致 5. 设计师分配弹窗从Product表加载空间,支持空间分配功能 ### 实现方式: - ✅ 订单分配:自动创建缺失的Product记录 - ✅ 交付执行:自动同步报价中的空间到Product表 - ✅ 设计师分配:从Product表加载空间,支持多设计师分配 ### 建议改进: - 添加报价保存时的Product同步 - 实现空间数据一致性验证 - 完善设计师空间分配的持久化存储