Browse Source

Merge branch 'master' of http://git.fmode.cn:3000/nkkj/yss-project

Future 22 hours ago
parent
commit
3dea75095c
4 changed files with 1282 additions and 171 deletions
  1. 14 9
      docs/prd/wxwork-project-management.md
  2. 1056 25
      docs/prd/项目-交付执行.md
  3. 159 98
      rules/schemas.md
  4. 53 39
      scripts/migration/create-schema.js

+ 14 - 9
docs/prd/wxwork-project-management.md

@@ -686,14 +686,19 @@ async initiateDelivery() {
     `【项目交付通知】\n${project.get('title')} 已完成全部设计工作,请查收成果。\n如有修改意见请及时反馈。`
   );
 
-  // 4. 创建尾款结算记录
-  const settlement = new Parse.Object('ProjectSettlement');
-  settlement.set('project', projectPointer);
-  settlement.set('stage', '尾款');
-  settlement.set('amount', finalAmount);
-  settlement.set('status', '待结算');
-  settlement.set('dueDate', new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)); // 7天后
-  await settlement.save();
+  // 4. 创建尾款付款记录
+  const payment = new Parse.Object('ProjectPayment');
+  payment.set('project', projectPointer);
+  payment.set('company', this.getCurrentUserCompany());
+  payment.set('type', 'final');
+  payment.set('stage', 'aftercare');
+  payment.set('amount', finalAmount);
+  payment.set('currency', 'CNY');
+  payment.set('status', 'pending');
+  payment.set('dueDate', new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)); // 7天后
+  payment.set('description', '项目尾款结算');
+  payment.set('recordedBy', Parse.User.current()?.toPointer());
+  await payment.save();
 }
 ```
 
@@ -742,7 +747,7 @@ async initiateDelivery() {
 
 **4.1 尾款管理**
 - 显示尾款金额和截止日期
-- 客户上传付款凭证(ProjectVoucher
+- 客户上传付款凭证(通过ProjectFile关联到ProjectPayment
 - OCR识别凭证信息(金额、时间)
 - 财务确认收款
 

+ 1056 - 25
docs/prd/项目-交付执行.md

@@ -1,33 +1,34 @@
-# 项目管理 - 交付执行阶段 PRD (Product表版本)
+# 项目管理 - 交付执行阶段 PRD (基于Product表和NovaStorage)
 
 ## 1. 功能概述
 
 ### 1.1 阶段定位
-交付执行阶段是项目管理流程的核心执行环节,包含建模、软装、渲染、后期四个连续子阶段。该阶段负责将设计方案转化为可交付的视觉成果,是项目价值实现的关键环节。
+交付执行阶段是项目管理流程的核心执行环节,包含**白模建模、软装设计、渲染输出、后期处理**四个连续子阶段。该阶段负责将设计方案转化为可交付的视觉成果,是项目价值实现的关键环节。
 
 ### 1.2 核心目标
-- **多产品设计协同管理**:支持单产品设计到多产品设计项目的灵活管理
-- **按产品设计维度组织文件上传和进度管理**
-- **实现四个执行阶段的串行推进**
-- **跨产品设计协调与依赖管理**:处理产品设计间的风格一致性、色彩流线、材质匹配
-- **提供实时进度跟踪和状态可视化**
-- **支持组长审核和质量把控**
-- **确保交付物符合质量标准**
+- **多空间产品协同管理**:基于Product表统一管理各空间设计产品的交付执行
+- **四阶段全流程管控**:白模→软装→渲染→后期的完整执行链路
+- **文件管理与存储集成**:基于NovaStorage的文件上传,prefixKeys=project/:pid/,Attachment与ProjectFile同步保存
+- **ProjectTeam协作管理**:根据团队成员角色和技能,合理分配各阶段任务
+- **交付物分类管理**:按四大核心内容(白模\软装\渲染\后期)分类管理交付物
+- **质量把控与审核**:支持组长审核和质量标准验证
+- **实时进度跟踪**:多维度进度监控和状态可视化
 
 ### 1.3 涉及角色
-- **设计师**:负责建模、软装、后期等设计工作
-- **渲染师**:负责渲染阶段的大图输出
-- **组长**:审核各阶段交付物、把控质量
-- **技术**:验收最终交付物、确认质量
+- **设计师**:负责白模建模、软装设计、后期处理等设计工作
+- **渲染师**:专门负责渲染阶段的图片输出和质量把控
+- **组长**:审核各阶段交付物、把控质量标准、团队协调
+- **技术**:验收最终交付物、确认技术规范和质量标准
+- **客服**:沟通客户需求、传递反馈信息、协调交付时间
 
 ### 1.4 四大执行子阶段
 
 ```mermaid
 graph LR
-    A[方案确认] --> B[建模]
-    B --> C[软装]
-    C --> D[渲染]
-    D --> E[后期]
+    A[方案确认] --> B[白模建模]
+    B --> C[软装设计]
+    C --> D[渲染输出]
+    D --> E[后期处理]
     E --> F[尾款结算]
 
     style B fill:#e3f2fd
@@ -36,11 +37,226 @@ graph LR
     style E fill:#f3e5f5
 ```
 
-## 2. 基于Product表的交付管理系统
+### 1.5 四大核心交付内容
+1. **白模建模**:空间结构建模、基础框架搭建
+2. **软装设计**:家具配置、材质选择、色彩搭配
+3. **渲染输出**:高清效果图、全景图、细节特写
+4. **后期处理**:色彩调整、效果优化、最终成品
 
-### 2.1 产品交付管理架构
+## 2. 基于Product表和ProjectTeam的交付管理系统
 
-#### 2.1.1 增强的DeliveryProcess接口
+### 2.1 交付执行管理架构
+
+#### 2.1.1 核心数据关系
+```typescript
+// Product产品表 - 空间设计产品核心
+interface Product {
+  objectId: string;
+  project: Pointer<Project>;
+  profile: Pointer<Profile>;          // 负责设计师
+  productName: string;                // 产品名称:李总主卧设计
+  productType: string;                // 空间类型:bedroom
+  stage: string;                      // 当前阶段:modeling/softDecor/rendering/postProcess
+  status: string;                     // 状态:not_started/in_progress/completed
+  quotation: Object;                  // 产品报价信息
+  requirements: Object;               // 设计需求
+  space: Object;                      // 空间信息
+  data: Object;                       // 扩展数据
+}
+
+// ProjectTeam项目团队表 - 团队成员管理
+interface ProjectTeam {
+  objectId: string;
+  project: Pointer<Project>;
+  profile: Pointer<Profile>;
+  role: string;                       // designer/renderer/team_leader/technical
+  workload: Number;                   // 工作量
+  specialties: string[];              // 专业技能
+  assignedProducts: string[];         // 分配的产品ID列表
+}
+
+// ProjectFile项目文件表 - 交付物管理
+interface ProjectFile {
+  objectId: string;
+  project: Pointer<Project>;
+  product: Pointer<Product>;          // 关联的空间产品
+  attach: Pointer<Attachment>;        // NovaStorage附件
+  category: string;                   // white_model/soft_decor/rendering/post_process
+  stage: string;                      // modeling/softDecor/rendering/postProcess
+  uploadedBy: Pointer<Profile>;
+  data: Object;                       // 文件元数据、审核状态等
+}
+```
+
+#### 2.1.2 基于NovaStorage的文件管理
+```typescript
+// 文件上传服务 - 使用NovaStorage
+class DeliveryFileService {
+  private storage: NovaStorage;
+
+  constructor() {
+    const cid = localStorage.getItem('company')!;
+    this.storage = NovaStorage.withCid(cid);
+  }
+
+  // 上传交付文件
+  async uploadDeliveryFile(
+    projectId: string,
+    productId: string,
+    category: string,
+    file: File
+  ): Promise<{ attachment: Parse.Object; projectFile: Parse.Object }> {
+
+    // 1. 上传到NovaStorage,使用项目前缀
+    const novaFile: NovaFile = await this.storage.upload(file, {
+      prefixKey: `project/${projectId}/`,
+      onProgress: (progress) => {
+        console.log('Upload progress:', progress.total.percent);
+      }
+    });
+
+    // 2. 创建Attachment记录
+    const attachment = new Parse.Object("Attachment");
+    attachment.set("name", novaFile.name);
+    attachment.set("url", novaFile.url);
+    attachment.set("size", novaFile.size);
+    attachment.set("mime", novaFile.type);
+    attachment.set("md5", novaFile.md5);
+    attachment.set("metadata", novaFile.metadata);
+    attachment.set("company", { __type: "Pointer", className: "Company", objectId: cid });
+    await attachment.save();
+
+    // 3. 创建ProjectFile记录
+    const projectFile = new Parse.Object("ProjectFile");
+    projectFile.set("project", { __type: "Pointer", className: "Project", objectId: projectId });
+    projectFile.set("product", { __type: "Pointer", className: "Product", objectId: productId });
+    projectFile.set("attach", { __type: "Pointer", className: "Attachment", objectId: attachment.id });
+    projectFile.set("category", category);      // white_model/soft_decor/rendering/post_process
+    projectFile.set("stage", this.mapCategoryToStage(category));
+    projectFile.set("uploadedBy", { __type: "Pointer", className: "Profile", objectId: currentUser.id });
+    projectFile.set("data", {
+      reviewStatus: "pending",
+      uploadTime: new Date(),
+      fileSize: novaFile.size,
+      fileType: novaFile.type
+    });
+    await projectFile.save();
+
+    return { attachment, projectFile };
+  }
+
+  // 映射category到stage
+  private mapCategoryToStage(category: string): string {
+    const mapping = {
+      'white_model': 'modeling',
+      'soft_decor': 'softDecor',
+      'rendering': 'rendering',
+      'post_process': 'postProcess'
+    };
+    return mapping[category] || 'modeling';
+  }
+}
+```
+
+### 2.2 基于ProjectTeam的团队协作管理
+
+#### 2.2.1 团队分配策略服务
+```typescript
+class DeliveryTeamManagementService {
+  // 智能分配团队成员到产品阶段
+  async assignTeamMembers(
+    projectId: string,
+    productId: string,
+    stage: string
+  ): Promise<TeamAssignmentResult> {
+
+    // 1. 获取项目团队成员
+    const teamQuery = new Parse.Query("ProjectTeam");
+    teamQuery.equalTo("project", { __type: "Pointer", className: "Project", objectId: projectId });
+    teamQuery.include("profile");
+    const teamMembers = await teamQuery.find();
+
+    // 2. 根据阶段和专业技能匹配
+    const stageRequirements = this.getStageRequirements(stage);
+    const suitableMembers = teamMembers.filter(member => {
+      const specialties = member.get("data")?.specialties || [];
+      const role = member.get("role");
+
+      return this.isMemberSuitableForStage(role, specialties, stage);
+    });
+
+    // 3. 考虑当前工作量
+    const availableMembers = await this.filterByWorkload(suitableMembers);
+
+    // 4. 分配主负责人和协作者
+    const assignment = this.createTeamAssignment(availableMembers, stageRequirements);
+
+    // 5. 更新Product的负责人
+    await this.updateProductAssignee(productId, assignment.primaryAssignee);
+
+    return assignment;
+  }
+
+  // 阶段需求定义
+  private getStageRequirements(stage: string): StageRequirements {
+    const requirements = {
+      modeling: {
+        primaryRole: 'designer',
+        requiredSkills: ['3d_modeling', 'autocad', 'sketchup'],
+        minHours: 8,
+        maxHours: 40,
+        complexity: 'medium'
+      },
+      softDecor: {
+        primaryRole: 'designer',
+        requiredSkills: ['interior_design', 'material_selection', 'color_theory'],
+        minHours: 6,
+        maxHours: 32,
+        complexity: 'high'
+      },
+      rendering: {
+        primaryRole: 'renderer',
+        requiredSkills: ['3ds_max', 'vray', 'corona', 'lumion', 'photoshop'],
+        minHours: 4,
+        maxHours: 24,
+        complexity: 'high'
+      },
+      postProcess: {
+        primaryRole: 'designer',
+        requiredSkills: ['photoshop', 'lightroom', 'color_correction'],
+        minHours: 3,
+        maxHours: 16,
+        complexity: 'low'
+      }
+    };
+
+    return requirements[stage] || requirements.modeling;
+  }
+
+  // 检查成员是否适合阶段
+  private isMemberSuitableForStage(
+    role: string,
+    specialties: string[],
+    stage: string
+  ): boolean {
+    const requirements = this.getStageRequirements(stage);
+
+    // 角色匹配
+    if (role !== requirements.primaryRole && role !== 'team_leader') {
+      return false;
+    }
+
+    // 技能匹配
+    const hasRequiredSkills = requirements.requiredSkills.some(skill =>
+      specialties.includes(skill)
+    );
+
+    return hasRequiredSkills;
+  }
+}
+```
+
+#### 2.2.2 增强的DeliveryProcess接口
 ```typescript
 interface ProductDeliveryProcess {
   id: string;                           // 流程ID: 'modeling' | 'softDecor' | 'rendering' | 'postProcess'
@@ -609,16 +825,831 @@ class BatchOperationService {
 }
 ```
 
-## 3. 交付执行界面设计
+## 3. 完整的交付执行界面设计
+
+### 3.1 交付执行主界面布局
 
-### 3.1 产品交付管理主界面
+#### 3.1.1 主界面结构
 ```html
-<!-- 产品交付管理主界面 -->
-<div class="product-delivery-container">
-  <!-- 阶段导航 -->
+<!-- 交付执行主界面 -->
+<div class="delivery-execution-container">
+  <!-- 顶部导航栏 -->
+  <div class="delivery-header">
+    <div class="project-info">
+      <h2>{{ project.title }}</h2>
+      <div class="project-meta">
+        <span class="customer">{{ project.customer.name }}</span>
+        <span class="deadline">截止:{{ formatDate(project.deadline) }}</span>
+      </div>
+    </div>
+
+    <div class="header-actions">
+      <button class="btn btn-primary" @click="showTeamManagement = true">
+        <i class="fas fa-users"></i>
+        团队管理
+      </button>
+      <button class="btn btn-secondary" @click="exportDeliveryReport">
+        <i class="fas fa-download"></i>
+        导出报告
+      </button>
+    </div>
+  </div>
+
+  <!-- 四阶段导航 -->
   <div class="stage-navigation">
     <div class="stage-tabs">
       <div v-for="stage in deliveryStages"
+           :key="stage.id"
+           class="stage-tab"
+           :class="{
+             active: activeStage === stage.id,
+             completed: stage.status === 'completed',
+             current: stage.status === 'in_progress'
+           }"
+           @click="switchStage(stage.id)">
+
+        <!-- 阶段图标和进度 -->
+        <div class="stage-icon">
+          <i :class="getStageIcon(stage.id)"></i>
+          <div class="stage-progress-ring" :style="getProgressStyle(stage.progress)"></div>
+        </div>
+
+        <!-- 阶段信息 -->
+        <div class="stage-info">
+          <h4>{{ getStageDisplayName(stage.id) }}</h4>
+          <div class="progress-info">
+            <div class="progress-bar">
+              <div class="progress-fill" :style="{ width: stage.progress + '%' }"></div>
+            </div>
+            <span class="progress-text">{{ stage.progress }}%</span>
+          </div>
+          <div class="stage-meta">
+            <span class="product-count">{{ stage.productCount }}个产品</span>
+            <span class="time-remaining">{{ stage.remainingDays }}天</span>
+          </div>
+        </div>
+
+        <!-- 团队成员状态 -->
+        <div class="team-status">
+          <div class="team-avatars">
+            <img v-for="member in stage.teamMembers"
+                 :key="member.id"
+                 :src="member.avatar"
+                 :title="member.name"
+                 class="team-avatar" />
+          </div>
+          <span class="team-status-text" :class="stage.teamStatus">
+            {{ getTeamStatusText(stage.teamStatus) }}
+          </span>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 批量操作工具栏 -->
+  <div class="batch-operations-toolbar">
+    <div class="selection-controls">
+      <label class="checkbox-wrapper">
+        <input type="checkbox"
+               v-model="selectAllProducts"
+               @change="toggleSelectAll">
+        <span class="checkmark"></span>
+        <span>全选</span>
+      </label>
+
+      <span v-if="selectedProducts.length > 0" class="selection-count">
+        已选择 {{ selectedProducts.length }} 个产品
+      </span>
+    </div>
+
+    <div class="batch-actions" v-if="selectedProducts.length > 0">
+      <!-- 文件上传 -->
+      <div class="upload-group">
+        <button class="btn btn-primary"
+                @click="showBatchFileUpload = true">
+          <i class="fas fa-upload"></i>
+          批量上传文件
+        </button>
+
+        <div class="upload-categories">
+          <button v-for="category in fileCategories"
+                  :key="category.id"
+                  class="btn-category"
+                  :class="category.id"
+                  @click="batchUploadCategory = category.id"
+                  :title="category.description">
+            <i :class="category.icon"></i>
+            {{ category.name }}
+          </button>
+        </div>
+      </div>
+
+      <!-- 团队分配 -->
+      <button class="btn btn-secondary"
+              @click="showTeamAssignment = true">
+        <i class="fas fa-user-plus"></i>
+        分配团队成员
+      </button>
+
+      <!-- 状态更新 -->
+      <button class="btn btn-info"
+              @click="showBatchStatusUpdate = true">
+        <i class="fas fa-edit"></i>
+        批量更新状态
+      </button>
+
+      <!-- 质量检查 -->
+      <button class="btn btn-warning"
+              @click="startBatchQualityCheck">
+        <i class="fas fa-check-circle"></i>
+        批量质检
+      </button>
+    </div>
+  </div>
+
+  <!-- 产品管理区域 -->
+  <div class="products-management-section">
+    <div class="section-header">
+      <h3>{{ getStageDisplayName(activeStage) }} - 产品管理</h3>
+
+      <!-- 视图切换 -->
+      <div class="view-controls">
+        <div class="view-toggle">
+          <button v-for="view in viewOptions"
+                  :key="view.id"
+                  class="view-btn"
+                  :class="{ active: currentView === view.id }"
+                  @click="currentView = view.id"
+                  :title="view.description">
+            <i :class="view.icon"></i>
+            {{ view.name }}
+          </button>
+        </div>
+
+        <!-- 筛选和排序 -->
+        <div class="filter-controls">
+          <select v-model="filterByStatus" class="filter-select">
+            <option value="all">全部状态</option>
+            <option value="not_started">未开始</option>
+            <option value="in_progress">进行中</option>
+            <option value="awaiting_review">待审核</option>
+            <option value="completed">已完成</option>
+          </select>
+
+          <select v-model="sortBy" class="sort-select">
+            <option value="priority">优先级</option>
+            <option value="deadline">截止时间</option>
+            <option value="progress">进度</option>
+            <option value="assignee">负责人</option>
+          </select>
+        </div>
+      </div>
+    </div>
+
+    <!-- 产品网格视图 -->
+    <div v-if="currentView === 'grid'" class="products-grid">
+      <div v-for="product in filteredProducts"
+           :key="product.productId"
+           class="product-card"
+           :class="{
+             selected: selectedProducts.includes(product.productId),
+             expanded: product.isExpanded,
+             'status-' + product.status
+           }"
+           @click="toggleProductSelection(product.productId, $event)">
+
+        <!-- 产品头部信息 -->
+        <div class="product-header">
+          <div class="product-basic-info">
+            <div class="product-icon">
+              <i :class="getProductTypeIcon(product.productType)"></i>
+            </div>
+
+            <div class="product-details">
+              <h4 class="product-name">{{ product.productName }}</h4>
+              <span class="product-type">{{ getProductTypeName(product.productType) }}</span>
+              <div class="space-info">
+                <span>{{ product.space?.area }}m²</span>
+                <span v-if="product.space?.priority === 'high'" class="priority-badge high">
+                  高优先级
+                </span>
+              </div>
+            </div>
+          </div>
+
+          <!-- 产品状态和操作 -->
+          <div class="product-status-section">
+            <div class="status-badge" :class="product.status">
+              {{ getStatusText(product.status) }}
+            </div>
+
+            <div class="product-actions">
+              <button class="action-btn"
+                      @click.stop="toggleProductExpansion(product.productId)"
+                      :title="product.isExpanded ? '收起' : '展开'">
+                <i :class="product.isExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
+              </button>
+
+              <label class="checkbox-wrapper">
+                <input type="checkbox"
+                       :value="product.productId"
+                       v-model="selectedProducts"
+                       @click.stop>
+                <span class="checkmark"></span>
+              </label>
+            </div>
+          </div>
+        </div>
+
+        <!-- 进度和团队信息 -->
+        <div class="product-progress-section">
+          <div class="progress-overview">
+            <div class="progress-bar">
+              <div class="progress-fill"
+                   :style="{ width: product.stageProgress[activeStage] + '%' }"
+                   :class="getProgressClass(product.stageProgress[activeStage])">
+              </div>
+            </div>
+            <span class="progress-text">{{ product.stageProgress[activeStage] }}%</span>
+          </div>
+
+          <div class="team-info">
+            <div class="assignee-info">
+              <img :src="product.assignee?.avatar"
+                   class="assignee-avatar"
+                   :title="product.assignee?.name" />
+              <span>{{ product.assignee?.name }}</span>
+            </div>
+
+            <div class="time-info">
+              <span v-if="product.estimatedHours" class="time-estimated">
+                预计 {{ product.estimatedHours }}h
+              </span>
+              <span v-if="product.actualHours" class="time-actual">
+                实际 {{ product.actualHours }}h
+              </span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 文件管理预览 -->
+        <div class="files-preview">
+          <div class="file-categories-preview">
+            <div v-for="category in fileCategories"
+                 :key="category.id"
+                 class="category-preview"
+                 :class="category.id">
+              <i :class="category.icon"></i>
+              <span class="file-count">{{ getFileCount(product, category.id) }}</span>
+            </div>
+          </div>
+
+          <div class="recent-files">
+            <div v-for="file in getRecentFiles(product)"
+                 :key="file.id"
+                 class="recent-file"
+                 :title="file.name">
+              <i :class="getFileIcon(file.name)"></i>
+            </div>
+          </div>
+        </div>
+
+        <!-- 展开的详细内容 -->
+        <div v-if="product.isExpanded" class="product-expanded-content">
+          <!-- 文件管理区域 -->
+          <div class="files-management">
+            <div class="files-header">
+              <h5>交付文件管理</h5>
+              <div class="file-upload-btn">
+                <input type="file"
+                       :id="'file-upload-' + product.productId"
+                       multiple
+                       @change="handleFileUpload($event, product.productId)"
+                       style="display: none;">
+                <button class="btn btn-sm btn-primary"
+                        @click="$event.stopPropagation(); triggerFileUpload(product.productId)">
+                  <i class="fas fa-plus"></i>
+                  上传文件
+                </button>
+              </div>
+            </div>
+
+            <!-- 分类文件列表 -->
+            <div class="files-by-category">
+              <div v-for="category in fileCategories"
+                   :key="category.id"
+                   class="category-section">
+                <div class="category-header">
+                  <h6>
+                    <i :class="category.icon"></i>
+                    {{ category.name }}
+                    <span class="count">({{ getFileCount(product, category.id) }})</span>
+                  </h6>
+                  <button class="btn-xs"
+                          @click="expandCategory(product.productId, category.id)"
+                          v-if="getFileCount(product, category.id) > 0">
+                    <i :class="expandedCategories[product.productId + '-' + category.id] ? 'fas fa-chevron-up' : 'fas fa-chevron-down'"></i>
+                  </button>
+                </div>
+
+                <div v-if="expandedCategories[product.productId + '-' + category.id] || getFileCount(product, category.id) <= 3"
+                     class="files-list">
+                  <div v-for="file in getFilesByCategory(product, category.id)"
+                       :key="file.id"
+                       class="file-item"
+                       :class="{
+                         'status-approved': file.reviewStatus === 'approved',
+                         'status-pending': file.reviewStatus === 'pending',
+                         'status-rejected': file.reviewStatus === 'rejected'
+                       }">
+                    <div class="file-preview">
+                      <img v-if="isImageFile(file.name)"
+                           :src="file.url"
+                           :alt="file.name"
+                           @error="handleImageError" />
+                      <i v-else :class="getFileIcon(file.name)"></i>
+                    </div>
+
+                    <div class="file-info">
+                      <span class="file-name" :title="file.name">{{ file.name }}</span>
+                      <span class="file-meta">
+                        {{ formatFileSize(file.size) }} • {{ formatDate(file.uploadTime) }}
+                      </span>
+                    </div>
+
+                    <div class="file-actions">
+                      <button class="action-btn"
+                              @click="previewFile(file)"
+                              title="预览">
+                        <i class="fas fa-eye"></i>
+                      </button>
+                      <button class="action-btn"
+                              @click="downloadFile(file)"
+                              title="下载">
+                        <i class="fas fa-download"></i>
+                      </button>
+
+                      <!-- 审核操作 -->
+                      <div v-if="canReviewFiles" class="review-actions">
+                        <button class="action-btn approve"
+                                @click="approveFile(file)"
+                                title="审核通过"
+                                :disabled="file.reviewStatus === 'approved'">
+                          <i class="fas fa-check"></i>
+                        </button>
+                        <button class="action-btn reject"
+                                @click="rejectFile(file)"
+                                title="驳回"
+                                :disabled="file.reviewStatus === 'rejected'">
+                          <i class="fas fa-times"></i>
+                        </button>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 备注和日志 -->
+          <div class="notes-and-logs">
+            <div class="notes-section">
+              <h6>阶段备注</h6>
+              <textarea class="notes-textarea"
+                        v-model="product.stageNotes[activeStage]"
+                        @blur="updateStageNotes(product.productId)"
+                        placeholder="添加当前阶段的备注信息..."
+                        rows="3"></textarea>
+
+              <div class="notes-meta">
+                <span class="last-updated">
+                  最后更新: {{ formatDateTime(product.lastUpdated) }}
+                </span>
+                <span class="updated-by">{{ product.lastUpdatedBy }}</span>
+              </div>
+            </div>
+
+            <div class="activity-logs">
+              <h6>活动日志</h6>
+              <div class="log-list">
+                <div v-for="log in getActivityLogs(product.productId)"
+                     :key="log.id"
+                     class="log-item">
+                  <div class="log-time">{{ formatTime(log.timestamp) }}</div>
+                  <div class="log-content">{{ log.message }}</div>
+                  <div class="log-user">{{ log.user }}</div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 产品列表视图(表格) -->
+    <div v-if="currentView === 'list'" class="products-table-container">
+      <table class="products-table">
+        <thead>
+          <tr>
+            <th class="checkbox-col">
+              <input type="checkbox" v-model="selectAllProducts" @change="toggleSelectAll">
+            </th>
+            <th>产品名称</th>
+            <th>空间类型</th>
+            <th>负责人</th>
+            <th>进度</th>
+            <th>文件数量</th>
+            <th>状态</th>
+            <th>预计时间</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="product in filteredProducts"
+              :key="product.productId"
+              class="product-row"
+              :class="{ 'status-' + product.status, selected: selectedProducts.includes(product.productId) }">
+            <td class="checkbox-col">
+              <input type="checkbox" :value="product.productId" v-model="selectedProducts">
+            </td>
+            <td>
+              <div class="product-cell">
+                <i :class="getProductTypeIcon(product.productType)"></i>
+                <span>{{ product.productName }}</span>
+              </div>
+            </td>
+            <td>{{ getProductTypeName(product.productType) }}</td>
+            <td>
+              <div class="assignee-cell">
+                <img :src="product.assignee?.avatar" class="avatar-sm">
+                <span>{{ product.assignee?.name }}</span>
+              </div>
+            </td>
+            <td>
+              <div class="progress-cell">
+                <div class="progress-bar-sm">
+                  <div class="progress-fill" :style="{ width: product.stageProgress[activeStage] + '%' }"></div>
+                </div>
+                <span>{{ product.stageProgress[activeStage] }}%</span>
+              </div>
+            </td>
+            <td>{{ getTotalFileCount(product) }}</td>
+            <td>
+              <span class="status-badge" :class="product.status">
+                {{ getStatusText(product.status) }}
+              </span>
+            </td>
+            <td>{{ product.estimatedHours }}h</td>
+            <td>
+              <div class="action-buttons">
+                <button class="action-btn" @click="viewProductDetails(product)" title="查看详情">
+                  <i class="fas fa-eye"></i>
+                </button>
+                <button class="action-btn" @click="editProduct(product)" title="编辑">
+                  <i class="fas fa-edit"></i>
+                </button>
+              </div>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</div>
+```
+
+### 3.2 核心组件设计
+
+#### 3.2.1 文件上传组件
+```typescript
+// 智能文件上传组件
+@Component({
+  selector: 'app-delivery-file-upload',
+  standalone: true,
+  imports: [CommonModule, FormsModule, IonIcon]
+})
+export class DeliveryFileUploadComponent {
+  @Input() projectId: string = '';
+  @Input() productId: string = '';
+  @Input() category: 'white_model' | 'soft_decor' | 'rendering' | 'post_process' = 'white_model';
+  @Output() fileUploaded = new EventEmitter<UploadResult>();
+
+  uploading: boolean = false;
+  uploadProgress: number = 0;
+  dragOver: boolean = false;
+
+  constructor(private fileService: DeliveryFileService) {}
+
+  async onFileSelect(event: Event): Promise<void> {
+    const files = (event.target as HTMLInputElement).files;
+    if (!files?.length) return;
+
+    await this.uploadFiles(Array.from(files));
+  }
+
+  async uploadFiles(files: File[]): Promise<void> {
+    this.uploading = true;
+    this.uploadProgress = 0;
+
+    try {
+      for (const file of files) {
+        const result = await this.fileService.uploadDeliveryFile(
+          this.projectId,
+          this.productId,
+          this.category,
+          file
+        );
+
+        this.fileUploaded.emit({
+          file: result.projectFile,
+          attachment: result.attachment,
+          category: this.category
+        });
+      }
+    } catch (error) {
+      console.error('File upload failed:', error);
+      // 显示错误提示
+    } finally {
+      this.uploading = false;
+      this.uploadProgress = 0;
+    }
+  }
+
+  getFileIcon(fileName: string): string {
+    const ext = fileName.split('.').pop()?.toLowerCase();
+    const iconMap = {
+      'jpg': 'fas fa-image',
+      'png': 'fas fa-image',
+      'gif': 'fas fa-image',
+      'pdf': 'fas fa-file-pdf',
+      'dwg': 'fas fa-file-cad',
+      'dxf': 'fas fa-file-cad',
+      'skp': 'fas fa-file-cad',
+      'max': 'fas fa-file-cad'
+    };
+    return iconMap[ext] || 'fas fa-file';
+  }
+}
+```
+
+#### 3.2.2 团队分配组件
+```typescript
+// 团队成员分配组件
+@Component({
+  selector: 'app-team-assignment',
+  standalone: true,
+  imports: [CommonModule, FormsModule, IonIcon]
+})
+export class TeamAssignmentComponent {
+  @Input() projectId: string = '';
+  @Input() stage: string = '';
+  @Input() selectedProducts: string[] = [];
+
+  availableTeamMembers: TeamMember[] = [];
+  assignmentSuggestions: AssignmentSuggestion[] = [];
+
+  constructor(private teamService: DeliveryTeamManagementService) {}
+
+  async loadTeamMembers(): Promise<void> {
+    const teamQuery = new Parse.Query("ProjectTeam");
+    teamQuery.equalTo("project", { __type: "Pointer", className: "Project", objectId: this.projectId });
+    teamQuery.include("profile");
+    const members = await teamQuery.find();
+
+    this.availableTeamMembers = members.map(member => ({
+      id: member.id,
+      name: member.get("profile").get("name"),
+      avatar: member.get("profile").get("data")?.avatar,
+      role: member.get("role"),
+      specialties: member.get("data")?.specialties || [],
+      currentWorkload: member.get("workload") || 0,
+      assignedProducts: member.get("data")?.assignedProducts || []
+    }));
+  }
+
+  async generateAssignmentSuggestions(): Promise<void> {
+    const requirements = this.getStageRequirements(this.stage);
+
+    this.assignmentSuggestions = this.selectedProducts.map(productId => {
+      const suitableMembers = this.availableTeamMembers.filter(member =>
+        this.isMemberSuitable(member, requirements)
+      );
+
+      return {
+        productId,
+        productName: this.getProductName(productId),
+        primaryAssignee: this.selectBestMember(suitableMembers),
+        collaborators: this.selectCollaborators(suitableMembers, 2),
+        reasoning: this.generateAssignmentReasoning(suitableMembers[0], requirements)
+      };
+    });
+  }
+
+  async applyAssignments(): Promise<void> {
+    for (const suggestion of this.assignmentSuggestions) {
+      await this.teamService.assignTeamMembers(
+        this.projectId,
+        suggestion.productId,
+        this.stage,
+        suggestion.primaryAssignee,
+        suggestion.collaborators
+      );
+    }
+  }
+}
+```
+
+#### 3.2.3 进度跟踪组件
+```typescript
+// 进度跟踪组件
+@Component({
+  selector: 'app-progress-tracker',
+  standalone: true,
+  imports: [CommonModule, FormsModule]
+})
+export class ProgressTrackerComponent {
+  @Input() productId: string = '';
+  @Input() stage: string = '';
+
+  progressData: ProgressData = {
+    current: 0,
+    target: 100,
+    milestones: [],
+    activities: []
+  };
+
+  constructor(private progressService: ProductProgressService) {}
+
+  async loadProgressData(): Promise<void> {
+    this.progressData = await this.progressService.getProductProgress(
+      this.productId,
+      this.stage
+    );
+  }
+
+  updateProgress(newProgress: number): void {
+    this.progressService.updateProductProgress(
+      this.productId,
+      this.stage,
+      { progress: newProgress }
+    );
+  }
+
+  addMilestone(milestone: Milestone): void {
+    this.progressData.milestones.push(milestone);
+    this.progressService.addMilestone(this.productId, this.stage, milestone);
+  }
+}
+```
+
+### 3.3 交互流程设计
+
+#### 3.3.1 文件上传交互
+1. **拖拽上传**:支持拖拽文件到指定区域
+2. **批量选择**:支持多文件同时上传
+3. **进度显示**:实时显示上传进度和状态
+4. **自动分类**:根据文件类型自动选择category
+5. **重试机制**:上传失败时支持重试
+
+#### 3.3.2 团队协作交互
+1. **智能推荐**:根据阶段需求自动推荐合适的团队成员
+2. **工作量平衡**:考虑当前工作量,避免过度分配
+3. **技能匹配**:基于专业技能进行精准匹配
+4. **协作支持**:支持主负责人+协作者模式
+
+#### 3.3.3 审核流程交互
+1. **预览功能**:支持文件预览和快速查看
+2. **审核操作**:通过/驳回/重新审核
+3. **批注功能**:支持在文件上添加批注
+4. **版本管理**:支持文件版本比较和回滚
+
+### 3.4 数据结构定义
+
+#### 3.4.1 文件分类结构
+```typescript
+interface FileCategory {
+  id: 'white_model' | 'soft_decor' | 'rendering' | 'post_process';
+  name: string;
+  icon: string;
+  description: string;
+  allowedTypes: string[];
+  maxSize: number; // MB
+}
+
+export const FILE_CATEGORIES: FileCategory[] = [
+  {
+    id: 'white_model',
+    name: '白模建模',
+    icon: 'fas fa-cube',
+    description: '空间结构建模、基础框架',
+    allowedTypes: ['.skp', '.max', '.dwg', '.dxf'],
+    maxSize: 50
+  },
+  {
+    id: 'soft_decor',
+    name: '软装设计',
+    icon: 'fas fa-couch',
+    description: '家具配置、材质选择、色彩搭配',
+    allowedTypes: ['.jpg', '.png', '.pdf', '.psd'],
+    maxSize: 20
+  },
+  {
+    id: 'rendering',
+    name: '渲染输出',
+    icon: 'fas fa-image',
+    description: '高清效果图、全景图、细节特写',
+    allowedTypes: ['.jpg', '.png', '.tiff', '.hdr'],
+    maxSize: 30
+  },
+  {
+    id: 'post_process',
+    name: '后期处理',
+    icon: 'fas fa-magic',
+    description: '色彩调整、效果优化、最终成品',
+    allowedTypes: ['.jpg', '.png', '.psd', '.tiff'],
+    maxSize: 25
+  }
+];
+```
+
+#### 3.4.2 团队角色定义
+```typescript
+interface TeamRole {
+  id: string;
+  name: string;
+  description: string;
+  requiredSkills: string[];
+  icon: string;
+  color: string;
+}
+
+export const TEAM_ROLES: TeamRole[] = [
+  {
+    id: 'designer',
+    name: '设计师',
+    description: '负责建模、软装、后期设计工作',
+    requiredSkills: ['autocad', 'sketchup', '3ds_max', 'photoshop'],
+    icon: 'fas fa-pencil-ruler',
+    color: '#007bff'
+  },
+  {
+    id: 'renderer',
+    name: '渲染师',
+    description: '负责效果图渲染和输出',
+    requiredSkills: ['3ds_max', 'vray', 'corona', 'lumion'],
+    icon: 'fas fa-palette',
+    color: '#28a745'
+  },
+  {
+    id: 'team_leader',
+    name: '组长',
+    description: '负责团队协调和质量把控',
+    requiredSkills: ['project_management', 'quality_control'],
+    icon: 'fas fa-users-cog',
+    color: '#ffc107'
+  },
+  {
+    id: 'technical',
+    name: '技术',
+    description: '负责技术验收和质量标准',
+    requiredSkills: ['technical_review', 'standards'],
+    icon: 'fas fa-tools',
+    color: '#6c757d'
+  }
+];
+```
+
+## 4. 技术实现要点
+
+### 4.1 性能优化策略
+- **虚拟滚动**:处理大量产品时的性能优化
+- **懒加载**:文件和详情按需加载
+- **缓存策略**:产品状态和文件信息缓存
+- **CDN加速**:文件预览和下载使用CDN
+
+### 4.2 用户体验优化
+- **响应式设计**:适配不同屏幕尺寸
+- **离线支持**:基本的离线操作和数据同步
+- **快捷键支持**:提高操作效率
+- **实时通知**:进度更新和状态变更通知
+
+### 4.3 数据一致性保障
+- **事务处理**:确保批量操作的原子性
+- **乐观锁**:防止并发修改冲突
+- **版本控制**:文件版本管理和回滚
+- **数据验证**:严格的前后端数据验证
+
+### 4.4 安全性考虑
+- **权限控制**:基于角色的访问控制
+- **文件安全**:文件上传安全检查和病毒扫描
+- **数据加密**:敏感数据传输和存储加密
+- **审计日志**:操作日志记录和追踪
+
+---
+
+**文档版本**: v4.0 (完整交付执行设计)
+**最后更新**: 2025-10-21
+**维护者**: YSS Development Team
            :key="stage.id"
            class="stage-tab"
            :class="{ active: activeStage === stage.id }"

+ 159 - 98
rules/schemas.md

@@ -176,28 +176,33 @@ TABLE(ProjectFile, "ProjectFile\n项目文件表") {
 }
 
 ' ============ 财务模块 ============
-TABLE(ProjectSettlement, "ProjectSettlement\n结算记录表") {
+TABLE(ProjectPayment, "ProjectPayment\n项目付款表") {
     FIELD(objectId, String)
     FIELD(project, Pointer→Project)
     FIELD(company, Pointer→Company)
+    FIELD(type, String)
     FIELD(stage, String)
+    FIELD(method, String)
     FIELD(amount, Number)
+    FIELD(currency, String)
     FIELD(percentage, Number)
-    FIELD(status, String)
+    FIELD(paymentDate, Date)
     FIELD(dueDate, Date)
-    FIELD(settledAt, Date)
-    FIELD(data, Object)
-    FIELD(isDeleted, Boolean)
-}
-
-TABLE(ProjectVoucher, "ProjectVoucher\n付款凭证表") {
-    FIELD(objectId, String)
-    FIELD(settlement, Pointer→ProjectSettlement)
-    FIELD(project, Pointer→Project)
-    FIELD(amount, Number)
+    FIELD(recordedDate, Date)
+    FIELD(status, String)
+    FIELD(voucherFile, Pointer→ProjectFile)
     FIELD(voucherUrl, String)
-    FIELD(recognizedInfo, Object)
+    FIELD(transactionId, String)
+    FIELD(paymentReference, String)
+    FIELD(paidBy, Pointer→ContactInfo)
+    FIELD(recordedBy, Pointer→Profile)
     FIELD(verifiedBy, Pointer→Profile)
+    FIELD(description, String)
+    FIELD(notes, String)
+    FIELD(relatedStage, String)
+    FIELD(product, Pointer→Product)
+    FIELD(autoReminderSent, Boolean)
+    FIELD(reminderCount, Number)
     FIELD(data, Object)
     FIELD(isDeleted, Boolean)
 }
@@ -271,8 +276,11 @@ Product "1" --> "n" ContactFollow : 产品跟进
 
 ' 交付与财务
 Project "1" --> "n" ProjectFile : 项目文件
-Project "1" --> "n" ProjectSettlement : 结算记录
-ProjectSettlement "1" --> "n" ProjectVoucher : 付款凭证
+Project "1" --> "n" ProjectPayment : 项目付款
+ProjectPayment "1" --> "1" ProjectFile : 付款凭证
+ProjectPayment "1" --> "1" ContactInfo : 付款人
+ProjectPayment "1" --> "1" Profile : 记录人/验证人
+Product "1" --> "n" ProjectPayment : 产品级付款
 
 ' 质量与沟通
 Project "1" --> "n" ProjectFeedback : 客户反馈
@@ -668,7 +676,142 @@ const quotationFiles = await quotationQuery.find();
 
 ---
 
-### 7. ProjectFeedback(客户反馈表)
+### 7. ProjectPayment(项目付款表)⭐
+
+**功能描述**: **统一的项目付款管理表**,整合了原有的 ProjectSettlement 和 ProjectVoucher 两张表的功能。每条记录代表一次具体的付款行为,包含付款类型、金额、方式、凭证等完整信息,支持项目级和产品级付款管理。
+
+**字段说明**:
+| 字段名 | 类型 | 必填 | 说明 | 示例值 |
+|--------|------|------|------|--------|
+| objectId | String | 是 | 主键ID | "payment001" |
+| project | Pointer | 是 | 所属项目 | → Project |
+| company | Pointer | 是 | 所属企业 | → Company |
+| **type** | **String** | **是** | **付款类型** | **"advance" / "milestone" / "final" / "refund"** |
+| **stage** | **String** | **是** | **付款阶段** | **"order" / "requirements" / "delivery" / "aftercare"** |
+| **method** | **String** | **是** | **支付方式** | **"cash" / "bank_transfer" / "alipay" / "wechat"** |
+| **amount** | **Number** | **是** | **付款金额** | **35000** |
+| **currency** | **String** | **是** | **货币类型** | **"CNY"** |
+| **percentage** | **Number** | **否** | **占总价百分比** | **30** |
+| **paymentDate** | **Date** | **否** | **实际付款时间** | **2024-12-01T10:00:00.000Z** |
+| **dueDate** | **Date** | **是** | **应付款时间** | **2024-12-01T00:00:00.000Z** |
+| **recordedDate** | **Date** | **自动** | **记录时间** | **2024-11-30T15:30:00.000Z** |
+| **status** | **String** | **是** | **付款状态** | **"pending" / "paid" / "overdue" / "cancelled"** |
+| **voucherFile** | **Pointer** | **否** | **付款凭证文件** | **→ ProjectFile** |
+| **voucherUrl** | **String** | **否** | **凭证URL** | **"https://..."** |
+| **transactionId** | **String** | **否** | **第三方交易ID** | **"wx_123456789"** |
+| **paymentReference** | **String** | **否** | **付款参考号/发票号** | **"INV-2024-001"** |
+| **paidBy** | **Pointer** | **是** | **付款人** | **→ ContactInfo** |
+| **recordedBy** | **Pointer** | **是** | **记录人** | **→ Profile** |
+| **verifiedBy** | **Pointer** | **否** | **验证人** | **→ Profile** |
+| **description** | **String** | **否** | **付款描述** | **"首期款"** |
+| **notes** | **String** | **否** | **备注信息** | **"客户通过银行转账付款"** |
+| **relatedStage** | **String** | **否** | **关联执行阶段** | **"modeling"** |
+| **product** | **Pointer** | **否** | **关联产品** | **→ Product** |
+| **autoReminderSent** | **Boolean** | **否** | **是否已发送提醒** | **false** |
+| **reminderCount** | **Number** | **否** | **提醒次数** | **0** |
+| **data** | **Object** | **否** | **扩展数据** | **{bankInfo, approvalFlow}** |
+| **isDeleted** | **Boolean** | **否** | **软删除标记** | **false** |
+
+**type 枚举值**:
+- `advance`: 预付款/定金
+- `milestone`: 里程碑付款
+- `final`: 尾款/结算款
+- `refund`: 退款
+
+**stage 枚举值**:
+- `order`: 订单分配阶段
+- `requirements`: 方案深化阶段
+- `delivery`: 交付执行阶段
+- `aftercare`: 售后归档阶段
+
+**method 枚举值**:
+- `cash`: 现金
+- `bank_transfer`: 银行转账
+- `alipay`: 支付宝
+- `wechat`: 微信支付
+- `credit_card`: 信用卡
+- `other`: 其他
+
+**status 枚举值**:
+- `pending`: 待付款
+- `paid`: 已付款
+- `overdue`: 逾期
+- `cancelled`: 已取消
+- `refunded`: 已退款
+
+**data 字段结构示例**:
+```json
+{
+  "bankInfo": {
+    "bankName": "中国工商银行",
+    "accountNumber": "****1234",
+    "accountName": "李四"
+  },
+  "approvalFlow": {
+    "requestedBy": "客服小王",
+    "approvedBy": "财务张三",
+    "approvedAt": "2024-11-30T16:00:00.000Z",
+    "comments": "款项已确认到账"
+  },
+  "automaticPayment": {
+    "provider": "alipay",
+    "gateway": "alipay_scan",
+    "qrCodeUrl": "https://..."
+  }
+}
+```
+
+**使用场景示例**:
+```typescript
+// 创建项目付款记录
+const ProjectPayment = Parse.Object.extend("ProjectPayment");
+const payment = new ProjectPayment();
+payment.set("project", project.toPointer());
+payment.set("company", company.toPointer());
+payment.set("type", "advance");
+payment.set("stage", "order");
+payment.set("method", "bank_transfer");
+payment.set("amount", 35000);
+payment.set("currency", "CNY");
+payment.set("percentage", 30);
+payment.set("dueDate", new Date("2024-12-01"));
+payment.set("paidBy", customer.toPointer());
+payment.set("recordedBy", profile.toPointer());
+payment.set("status", "pending");
+payment.set("description", "项目首期款");
+await payment.save();
+
+// 上传付款凭证并关联
+const voucherFile = await uploadVoucherFile(file);
+payment.set("voucherFile", voucherFile.toPointer());
+payment.set("status", "paid");
+payment.set("paymentDate", new Date());
+await payment.save();
+
+// 查询项目的所有付款记录
+const paymentQuery = new Parse.Query("ProjectPayment");
+paymentQuery.equalTo("project", projectId);
+paymentQuery.include("voucherFile");
+paymentQuery.include("paidBy");
+paymentQuery.include("recordedBy");
+paymentQuery.descending("paymentDate");
+const payments = await paymentQuery.find();
+
+// 查询某个产品的付款记录
+const productPaymentQuery = new Parse.Query("ProjectPayment");
+productPaymentQuery.equalTo("product", { __type: "Pointer", className: "Product", objectId: productId });
+productPaymentQuery.equalTo("status", "paid");
+const productPayments = await productPaymentQuery.find();
+
+// 计算项目总付款金额
+const totalPaid = payments.reduce((sum, payment) => {
+  return sum + (payment.get("status") === "paid" ? payment.get("amount") : 0);
+}, 0);
+```
+
+---
+
+### 8. ProjectFeedback(客户反馈表)
 
 **用途**: 记录客户在各阶段的反馈和评价。**已更新支持产品关联**。
 
@@ -725,85 +868,3 @@ const products = await productQuery.equalTo("project", projectId).find();
 // 每个product包含完整的space、quotation、requirements信息
 // 文件通过ProjectFile按fileCategory分类查询
 ```
-
----
-
-## 数据迁移指南
-
-### 从ProjectSpace多表结构迁移到Product表
-
-```typescript
-// 1. 迁移ProjectSpace到Product
-const oldSpaces = await new Parse.Query("ProjectSpace").find();
-for (const space of oldSpaces) {
-  const product = new Parse.Object("Product");
-  product.set("project", space.get("project"));
-  product.set("profile", space.get("assignedTeam")?.[0]?.profile); // 取第一个设计师
-  product.set("productName", `${space.get("project").get("title")}-${space.get("name")}`);
-  product.set("productType", space.get("type"));
-  product.set("space", {
-    spaceName: space.get("name"),
-    area: space.get("area"),
-    dimensions: space.get("metadata")?.dimensions,
-    features: space.get("metadata")?.features
-  });
-  product.set("quotation", space.get("quotation"));
-  product.set("requirements", space.get("requirements"));
-  product.set("reviews", space.get("reviews"));
-  product.set("estimatedBudget", space.get("estimatedBudget"));
-  product.set("estimatedDuration", space.get("estimatedDuration"));
-  product.set("order", space.get("order"));
-  await product.save();
-}
-
-// 2. 迁移ProjectFile,添加fileCategory分类
-const files = await new Parse.Query("ProjectFile").find();
-for (const file of files) {
-  // 根据文件名和内容自动分类
-  const fileName = file.get("fileName").toLowerCase();
-  let fileCategory = "other";
-
-  if (fileName.includes("报价") || fileName.includes("quotation")) {
-    fileCategory = "quotation";
-  } else if (fileName.includes("全景") || fileName.includes("panorama")) {
-    fileCategory = "panorama";
-  } else if (fileName.includes("效果图") || fileName.includes("render")) {
-    fileCategory = "delivery";
-  } else if (fileName.includes("参考") || fileName.includes("reference")) {
-    fileCategory = "reference";
-  }
-
-  file.set("fileCategory", fileCategory);
-  await file.save();
-}
-
-// 3. 删除不需要的表
-// - ProjectSpace
-// - SpaceQuotation
-// - SpaceRequirement
-// - SpaceReview
-// - ProductCheck
-// - PanoramaCollection
-// - SpacePanorama
-```
-
----
-
-## 总结
-
-通过Product表统一空间管理的重构,YSS项目管理系统实现了:
-
-✅ **语义清晰**:Product即空间设计产品,符合行业认知
-✅ **架构极简**:从15个空间相关表简化为1个Product表
-✅ **功能完整**:保留所有空间管理功能,语义更准确
-✅ **性能提升**:单表查询替代复杂JOIN操作
-✅ **维护便利**:降低数据模型维护成本
-✅ **扩展灵活**:Object字段支持未来功能扩展
-
-这个基于Product表的统一空间管理方案完美解决了多空间项目管理的复杂性问题,通过Product这一天然的空间设计产品载体,实现了报价、全景图、团队协作、文件管理、需求跟踪、评价反馈等全生命周期管理,大大简化了系统架构并提高了开发效率。
-
----
-
-**文档版本**: v3.0(Product表统一空间管理)
-**最后更新**: 2025-10-20
-**维护者**: YSS Development Team

+ 53 - 39
scripts/migration/create-schema.js

@@ -177,25 +177,36 @@ const tableConfigs = [
     ]
   },
 
-  // 交付物表:Product
+  // 空间设计产品表:Product
   {
     className: 'Product',
     fields: [
       { name: 'project', type: 'Pointer', required: true, targetClass: 'Project' },
       { name: 'company', type: 'Pointer', required: true, targetClass: 'Company' },
-      { name: 'stage', type: 'String', required: true },
+      { name: 'profile', type: 'Pointer', required: true, targetClass: 'Profile' },
+      { name: 'productName', type: 'String', required: true },
+      { name: 'productType', type: 'String', required: true },
+      { name: 'stage', type: 'String', required: true, defaultValue: 'not_started' },
       { name: 'processType', type: 'String', required: false },
-      { name: 'space', type: 'String', required: false },
-      { name: 'fileUrl', type: 'String', required: true },
+      { name: 'status', type: 'String', required: true, defaultValue: 'not_started' },
+      { name: 'fileUrl', type: 'String', required: false },
       { name: 'reviewStatus', type: 'String', required: true, defaultValue: 'pending' },
+      { name: 'space', type: 'Object', required: false, defaultValue: {} },
       { name: 'quotation', type: 'Object', required: false, defaultValue: {} },
+      { name: 'requirements', type: 'Object', required: false, defaultValue: {} },
+      { name: 'reviews', type: 'Array', required: false, defaultValue: [] },
+      { name: 'estimatedBudget', type: 'Number', required: false },
+      { name: 'estimatedDuration', type: 'Number', required: false },
+      { name: 'order', type: 'Number', required: false, defaultValue: 0 },
       { name: 'data', type: 'Object', required: false, defaultValue: {} },
       { name: 'isDeleted', type: 'Boolean', required: false, defaultValue: false }
     ],
     indexes: [
-      { name: 'project_stage_isDeleted', fields: { project: 1, stage: 1, isDeleted: 1 } },
-      { name: 'project_space', fields: { project: 1, space: 1 } },
-      { name: 'reviewStatus_project', fields: { reviewStatus: 1, project: 1 } }
+      { name: 'project_company_isDeleted', fields: { project: 1, company: 1, isDeleted: 1 } },
+      { name: 'profile_company', fields: { profile: 1, company: 1 } },
+      { name: 'productType_order', fields: { productType: 1, order: 1 } },
+      { name: 'stage_status', fields: { stage: 1, status: 1 } },
+      { name: 'reviewStatus', fields: { reviewStatus: 1 } }
     ]
   },
 
@@ -204,59 +215,62 @@ const tableConfigs = [
     className: 'ProjectFile',
     fields: [
       { name: 'project', type: 'Pointer', required: true, targetClass: 'Project' },
+      { name: 'product', type: 'Pointer', required: false, targetClass: 'Product' },
+      { name: 'attach', type: 'Pointer', required: true, targetClass: 'Attachment' },
       { name: 'uploadedBy', type: 'Pointer', required: true, targetClass: 'Profile' },
-      { name: 'fileType', type: 'String', required: true },
-      { name: 'fileUrl', type: 'String', required: true },
-      { name: 'fileName', type: 'String', required: true },
-      { name: 'fileSize', type: 'Number', required: false, defaultValue: 0 },
       { name: 'stage', type: 'String', required: false },
+      { name: 'category', type: 'String', required: false, defaultValue: 'other' },
       { name: 'data', type: 'Object', required: false, defaultValue: {} },
+      { name: 'analysis', type: 'Object', required: false, defaultValue: {} },
       { name: 'isDeleted', type: 'Boolean', required: false, defaultValue: false }
     ],
     indexes: [
-      { name: 'project_fileType_isDeleted', fields: { project: 1, fileType: 1, isDeleted: 1 } },
-      { name: 'uploadedBy_project', fields: { uploadedBy: 1, project: 1 } }
+      { name: 'project_product_stage_isDeleted', fields: { project: 1, product: 1, stage: 1, isDeleted: 1 } },
+      { name: 'attach', fields: { attach: 1 } },
+      { name: 'uploadedBy_project', fields: { uploadedBy: 1, project: 1 } },
+      { name: 'category_index', fields: { category: 1 } }
     ]
   },
 
-  // 结算记录表:ProjectSettlement
+  // 项目付款表:ProjectPayment ⭐
   {
-    className: 'ProjectSettlement',
+    className: 'ProjectPayment',
     fields: [
       { name: 'project', type: 'Pointer', required: true, targetClass: 'Project' },
       { name: 'company', type: 'Pointer', required: true, targetClass: 'Company' },
+      { name: 'type', type: 'String', required: true },
       { name: 'stage', type: 'String', required: true },
+      { name: 'method', type: 'String', required: true },
       { name: 'amount', type: 'Number', required: true },
+      { name: 'currency', type: 'String', required: true, defaultValue: 'CNY' },
       { name: 'percentage', type: 'Number', required: false, defaultValue: 0 },
-      { name: 'status', type: 'String', required: true, defaultValue: '待结算' },
-      { name: 'dueDate', type: 'Date', required: false },
-      { name: 'settledAt', type: 'Date', required: false },
-      { name: 'data', type: 'Object', required: false, defaultValue: {} },
-      { name: 'isDeleted', type: 'Boolean', required: false, defaultValue: false }
-    ],
-    indexes: [
-      { name: 'project_stage', fields: { project: 1, stage: 1 }, unique: true },
-      { name: 'status_company', fields: { status: 1, company: 1 } },
-      { name: 'dueDate_index', fields: { dueDate: 1 } }
-    ]
-  },
-
-  // 付款凭证表:ProjectVoucher
-  {
-    className: 'ProjectVoucher',
-    fields: [
-      { name: 'settlement', type: 'Pointer', required: true, targetClass: 'ProjectSettlement' },
-      { name: 'project', type: 'Pointer', required: true, targetClass: 'Project' },
-      { name: 'amount', type: 'Number', required: true },
-      { name: 'voucherUrl', type: 'String', required: true },
-      { name: 'recognizedInfo', type: 'Object', required: false, defaultValue: {} },
+      { name: 'paymentDate', type: 'Date', required: false },
+      { name: 'dueDate', type: 'Date', required: true },
+      { name: 'recordedDate', type: 'Date', required: false },
+      { name: 'status', type: 'String', required: true, defaultValue: 'pending' },
+      { name: 'voucherFile', type: 'Pointer', required: false, targetClass: 'ProjectFile' },
+      { name: 'voucherUrl', type: 'String', required: false },
+      { name: 'transactionId', type: 'String', required: false },
+      { name: 'paymentReference', type: 'String', required: false },
+      { name: 'paidBy', type: 'Pointer', required: true, targetClass: 'ContactInfo' },
+      { name: 'recordedBy', type: 'Pointer', required: true, targetClass: 'Profile' },
       { name: 'verifiedBy', type: 'Pointer', required: false, targetClass: 'Profile' },
+      { name: 'description', type: 'String', required: false },
+      { name: 'notes', type: 'String', required: false },
+      { name: 'relatedStage', type: 'String', required: false },
+      { name: 'product', type: 'Pointer', required: false, targetClass: 'Product' },
+      { name: 'autoReminderSent', type: 'Boolean', required: false, defaultValue: false },
+      { name: 'reminderCount', type: 'Number', required: false, defaultValue: 0 },
       { name: 'data', type: 'Object', required: false, defaultValue: {} },
       { name: 'isDeleted', type: 'Boolean', required: false, defaultValue: false }
     ],
     indexes: [
-      { name: 'settlement_project', fields: { settlement: 1, project: 1 } },
-      { name: 'project_isDeleted', fields: { project: 1, isDeleted: 1 } }
+      { name: 'project_company_isDeleted', fields: { project: 1, company: 1, isDeleted: 1 } },
+      { name: 'status_stage', fields: { status: 1, stage: 1 } },
+      { name: 'type_status', fields: { type: 1, status: 1 } },
+      { name: 'product_productType', fields: { product: 1, productType: 1 } },
+      { name: 'dueDate_index', fields: { dueDate: 1 } },
+      { name: 'recordedDate_desc', fields: { recordedDate: -1 } }
     ]
   },