Răsfoiți Sursa

fix: schema with ProjectPayment

ryanemax 1 zi în urmă
părinte
comite
7d25469810
3 a modificat fișierele cu 226 adăugiri și 99 ștergeri
  1. 14 9
      docs/prd/wxwork-project-management.md
  2. 159 51
      rules/schemas.md
  3. 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识别凭证信息(金额、时间)
 - 财务确认收款
 

+ 159 - 51
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,38 +868,3 @@ const products = await productQuery.equalTo("project", projectId).find();
 // 每个product包含完整的space、quotation、requirements信息
 // 文件通过ProjectFile按fileCategory分类查询
 ```
-
----
-
-## 数据迁移指南
-
-### Product表 空间设计产品
-
-```typescript
-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();
-}
-
-```
-
----
-
-
-这个基于Product表的统一空间管理方案完美解决了多空间项目管理的复杂性问题,通过Product这一天然的空间设计产品载体,实现了报价、全景图、团队协作、文件管理、需求跟踪、评价反馈等全生命周期管理,大大简化了系统架构并提高了开发效率。

+ 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 } }
     ]
   },