# 表单组织架构设计说明

## 📋 核心设计理念

**通过组织统一管理表单的全生命周期，彻底去除区域相关的冗余配置。**

## 🏗️ 架构原则

### 单一职责

| 概念 | 职责 | 实现 |
|-----|------|------|
| **Organization** | 决定范围 | 表单归属组织，组织关联区域 |
| **ReleaseSnapshot** | 版本管理 | 一个表单定义一个活动快照 |
| **FormInstance** | 记录环境 | 记录提交时的用户区域 |

### 架构层次

```
FormDefinition (表单定义)
├── organizationId = null  → 平台级通用表单
│   ├── 所有组织可用
│   ├── 所有区域可用
│   └── 一个 ACTIVE ReleaseSnapshot
│
└── organizationId = "xxx" → 组织专属表单
    ├── 仅该组织成员可用
    ├── 可用区域 = organization.departmentRegions
    └── 一个 ACTIVE ReleaseSnapshot
```

## 📊 数据模型详解

### 1. FormDefinition（表单定义）

```prisma
model FormDefinition {
  id             String
  key            String
  name           String
  category       String
  status         FormStatus
  
  // ✅ 核心字段：组织归属
  organizationId String?      // null = 平台级，非null = 组织专属
  organization   Department?  // 关联到组织（顶层部门）
  
  // ❌ 已删除的冗余字段
  // regionScope        FormRegionScope
  // allowedRegions     Json?
  // regionApprovalConfig Json?
}
```

**查询逻辑：**

```typescript
// 获取用户可用的表单列表
async function getAvailableForms(userId: string, userOrgId: string, userRegionId: string) {
  const forms = await prisma.formDefinition.findMany({
    where: {
      OR: [
        // 1. 平台级表单：所有人可用
        { organizationId: null },
        
        // 2. 用户所属组织的表单
        { organizationId: userOrgId }
      ],
      status: 'PUBLISHED'
    },
    include: {
      organization: {
        include: {
          departmentRegions: true  // 获取组织的运营区域
        }
      }
    }
  });
  
  // 应用层过滤：组织专属表单需要在用户所在区域运营
  return forms.filter(form => {
    if (!form.organizationId) {
      // 平台级表单：直接可用
      return true;
    }
    
    // 组织专属表单：检查组织是否在用户区域运营
    const orgRegions = form.organization.departmentRegions.map(r => r.regionId);
    return orgRegions.includes(userRegionId);
  });
}
```

### 2. ReleaseSnapshot（发布快照）

```prisma
model ReleaseSnapshot {
  id                  String
  formDefinitionId    String
  formVersionId       String
  processVersionId    String?
  status              ReleaseSnapshotStatus
  
  // ❌ 已删除
  // regionId String
  
  // ✅ 唯一约束：每个表单定义只有一个 ACTIVE 快照
  @@unique([formDefinitionId, status])
}
```

**设计理念：**

1. **一个表单定义 = 一个活动快照**
   - 简化版本管理
   - 避免区域版本不一致的问题

2. **如果需要区域差异，正确的做法是：**
   - 创建不同的表单定义（绑定不同区域的组织）
   - 使用多语言功能
   - 使用条件字段（表单内根据区域显示不同内容）

3. **发布流程简化：**

```typescript
async function publishSnapshot(
  formDefinitionId: string,
  formVersionId: string,
  processVersionId: string
) {
  // 1. 验证表单定义存在
  const form = await prisma.formDefinition.findUnique({
    where: { id: formDefinitionId },
    include: { organization: true }
  });
  
  if (!form) {
    throw new Error('表单定义不存在');
  }
  
  // 2. 归档旧的 ACTIVE 快照（如果存在）
  await prisma.releaseSnapshot.updateMany({
    where: {
      formDefinitionId,
      status: 'ACTIVE'
    },
    data: {
      status: 'ARCHIVED'
    }
  });
  
  // 3. 创建新的 ACTIVE 快照
  return prisma.releaseSnapshot.create({
    data: {
      formDefinitionId,
      formVersionId,
      processVersionId,
      status: 'ACTIVE',
      publishedAt: new Date()
    }
  });
}
```

**查询活动快照：**

```typescript
async function getActiveSnapshot(formDefinitionId: string) {
  return prisma.releaseSnapshot.findUnique({
    where: {
      formDefinitionId_status: {
        formDefinitionId,
        status: 'ACTIVE'
      }
    },
    include: {
      formVersion: true,
      processVersion: true
    }
  });
}
```

### 3. FormInstance（表单实例）

```prisma
model FormInstance {
  id                 String
  formDefinitionId   String
  formVersionId      String
  snapshotId         String?
  businessKey        String
  
  // ✅ 保留！记录提交时的区域环境
  regionId           String?
  
  data               Json
  status             FormInstanceStatus
  createdBy          String
  submittedBy        String?
}
```

**regionId 的用途：**

1. **数据归属标记** - 记录实例创建时的区域环境
2. **区域数据隔离** - 支持按区域查询和统计
3. **审批流路由** - 不同区域可能有不同的审批人
4. **数据分析** - 区域级的表单使用统计

**实例创建（简化版）：**

```typescript
async function createFormInstance(
  formDefinitionId: string,
  userId: string,
  userRegionId: string,
  data: any
) {
  // 获取活动快照（全局唯一）
  const snapshot = await prisma.releaseSnapshot.findUnique({
    where: {
      formDefinitionId_status: {
        formDefinitionId,
        status: 'ACTIVE'
      }
    }
  });
  
  if (!snapshot) {
    throw new Error('表单未发布');
  }
  
  return prisma.formInstance.create({
    data: {
      formDefinitionId,
      formVersionId: snapshot.formVersionId,
      snapshotId: snapshot.id,
      regionId: userRegionId,  // 记录提交时的区域
      businessKey: generateBusinessKey(),
      data,
      status: 'DRAFT',
      createdBy: userId
    }
  });
}
```

### 4. FormWebhook（Webhook 订阅）

```prisma
model FormWebhook {
  id             String
  organizationId String?     // null = 平台级，非null = 组织级
  organization   Department?
  name           String
  url            String
  events         Json
}
```

**采用 organizationId 的原因：**

1. **与表单架构一致** - 表单绑定组织，Webhook 也绑定组织
2. **简化订阅逻辑** - 订阅组织的所有表单事件
3. **区域信息在 payload** - 事件数据中包含 regionId，Webhook 端点可以根据内容决定处理逻辑

**订阅场景：**

```typescript
// 1. 平台级 Webhook - 订阅所有表单事件
FormWebhook {
  organizationId: null,
  url: "https://platform.example.com/webhook",
  events: ["form.instance.created", "form.snapshot.published"]
}

// 2. 组织级 Webhook - 订阅特定组织的表单事件
FormWebhook {
  organizationId: "org-us-uuid",
  url: "https://us.example.com/webhook",
  events: ["form.instance.submitted"]
}

// 3. 事件 payload 包含区域信息
{
  event: "form.instance.created",
  data: {
    instanceId: "xxx",
    formDefinitionId: "xxx",
    organizationId: "org-us-uuid",
    regionId: "region-ca-uuid",  // 由 FormInstance.regionId 提供
    createdBy: "user-uuid"
  }
}
```

## 🎯 设计优势

### 1. **极致简化**
```
删除前：
- FormDefinition: regionScope, allowedRegions, regionApprovalConfig
- ReleaseSnapshot: regionId（区域级版本管理）
- FormWebhook: regionId（区域级订阅）
- FormRegionScope 枚举

删除后：
- FormDefinition: 仅 organizationId
- ReleaseSnapshot: 无区域概念
- FormWebhook: 仅 organizationId
- 版本管理：全局唯一 ACTIVE 快照
```

### 2. **逻辑清晰**
```
平台级表单 → 全局可用，一个版本
组织专属表单 → 组织可用，一个版本
```

### 3. **避免版本混乱**
- ❌ 删除前：CN 用 v1.0，US 用 v1.1，ME 用 v1.0 → 混乱！
- ✅ 删除后：全球统一版本 → 清晰！

### 4. **发布流程简化**
- ❌ 删除前：需要为每个区域分别发布快照
- ✅ 删除后：一次发布，全球生效

## 📋 完整的架构清理

| 表 | regionId | 最终状态 | 说明 |
|---|---|---|---|
| **FormDefinition** | ❌ | 已删除 | 改为 organizationId |
| **ReleaseSnapshot** | ❌ | 已删除 | 全局版本管理 |
| **FormInstance** | ✅ | 保留 | 记录提交环境 |
| **FormWebhook** | ❌ | **已删除** | 改为 organizationId |

删除的所有字段：
- `FormDefinition.regionScope` - 删除
- `FormDefinition.allowedRegions` - 删除
- `FormDefinition.regionApprovalConfig` - 删除
- `FormRegionScope` 枚举 - 删除
- `ReleaseSnapshot.regionId` - 删除
- `FormWebhook.regionId` - **新删除，改为 organizationId**

## 📋 区域差异的正确处理方式

如果真的需要区域级差异，应该：

### 方案 1：创建不同的表单定义
```typescript
// 为不同区域的组织创建专属表单
FormDefinition {
  name: "US Expense Report"
  organizationId: "org-us"  // 美国组织
}

FormDefinition {
  name: "CN 报销申请"
  organizationId: "org-cn"  // 中国组织
}
```

### 方案 2：使用多语言功能
```typescript
FormVersion {
  nameI18n: {
    "zh-CN": "报销申请",
    "en-US": "Expense Report"
  }
  schema: {
    // 相同的字段结构
  }
}
```

### 方案 3：使用条件字段
```typescript
// 表单 schema 中根据区域显示不同字段
{
  type: "object",
  properties: {
    region: { type: "string" },  // 自动填充用户区域
    amount: {
      type: "number",
      // 根据区域显示不同的货币单位
    }
  }
}
```

## 🔄 迁移建议

### 数据库迁移

```sql
-- 1. 检查现有快照数据
SELECT 
  form_definition_id,
  COUNT(*) as snapshot_count,
  COUNT(DISTINCT region_id) as region_count
FROM release_snapshots
WHERE status = 'ACTIVE'
GROUP BY form_definition_id
HAVING COUNT(*) > 1;

-- 2. 对于每个表单定义，保留最新的快照，归档其他
-- （需要业务逻辑判断，这里只是示例）

-- 3. 删除 region_id 字段
ALTER TABLE release_snapshots DROP COLUMN region_id;

-- 4. 添加唯一约束
ALTER TABLE release_snapshots 
ADD CONSTRAINT release_snapshots_form_definition_id_status_key 
UNIQUE (form_definition_id, status);
```

### 代码修改清单

**后端 Service：**
- [x] 删除 `FormDefinition` 的区域配置字段
- [ ] 删除 `ReleaseSnapshot` 的 `regionId` 字段
- [ ] 修改发布快照逻辑，不再需要 regionId 参数
- [ ] 修改查询活动快照逻辑，使用唯一约束
- [ ] 简化表单可用性检查逻辑

**前端：**
- [ ] 删除 `FormDefinition` 类型中的区域字段
- [ ] 删除 `ReleaseSnapshot` 类型中的 `regionId`
- [ ] 简化发布流程 UI（不再需要选择区域）

## 📝 总结

这次彻底的架构简化实现了：

1. **删除 FormDefinition 的区域配置** - 3 个字段 + 1 个枚举
2. **删除 ReleaseSnapshot 的 regionId** - 区域级版本管理 → 全局版本管理
3. **删除 FormWebhook 的 regionId** - 改为 organizationId，与表单架构一致
4. **保留 FormInstance 的 regionId** - 记录环境，支持统计
5. **统一 ApprovalDefinition 架构** - 删除 regionScope/tenantId/orgScope，改为 organizationId
6. **添加 ApprovalInstance 的 regionId** - 与 FormInstance 保持一致

### 核心原则

```
组织决定范围 → FormDefinition.organizationId、FormWebhook.organizationId、ApprovalDefinition.organizationId
快照管理版本 → ReleaseSnapshot（全局统一）
实例记录环境 → FormInstance.regionId、ApprovalInstance.regionId
```

### 📊 完整的架构统一

| 模型 | organizationId | regionId | 说明 |
|-----|---------------|----------|------|
| **FormDefinition** | ✅ | ❌ | 表单归属组织 |
| **ApprovalDefinition** | ✅ | ❌ | 流程归属组织 |
| **ReleaseSnapshot** | - | ❌ | 全局版本管理 |
| **FormInstance** | - | ✅ | 记录提交环境 |
| **ApprovalInstance** | - | ✅ | 记录发起环境 |
| **FormWebhook** | ✅ | ❌ | 订阅组织事件 |

### 架构优势

- ✅ **更简单** - 无区域版本管理复杂度
- ✅ **更清晰** - 一个表单/流程一个版本
- ✅ **更一致** - 表单、流程、Webhook 都使用 organizationId
- ✅ **更好维护** - 发布流程大幅简化
- ✅ **更灵活** - Webhook 事件 payload 中包含区域信息，订阅方可自行决定处理逻辑
- ✅ **统一模式** - 表单和审批流程使用完全相同的组织+区域架构

这是一个非常彻底和优雅的架构设计！🎉


## 📊 数据模型详解

### 1. FormDefinition（表单定义）

```prisma
model FormDefinition {
  id             String
  key            String
  name           String
  category       String
  status         FormStatus
  
  // ✅ 核心字段：组织归属
  organizationId String?      // null = 平台级，非null = 组织专属
  organization   Department?  // 关联到组织（顶层部门）
  
  // ❌ 已删除的冗余字段
  // regionScope        FormRegionScope
  // allowedRegions     Json?
  // regionApprovalConfig Json?
}
```

**查询逻辑：**

```typescript
// 获取用户可用的表单列表
async function getAvailableForms(userId: string, userOrgId: string, userRegionId: string) {
  const forms = await prisma.formDefinition.findMany({
    where: {
      OR: [
        // 1. 平台级表单：所有人可用
        { organizationId: null },
        
        // 2. 用户所属组织的表单
        { organizationId: userOrgId }
      ],
      status: 'PUBLISHED'
    },
    include: {
      organization: {
        include: {
          departmentRegions: true  // 获取组织的运营区域
        }
      }
    }
  });
  
  // 应用层过滤：组织专属表单需要在用户所在区域运营
  return forms.filter(form => {
    if (!form.organizationId) {
      // 平台级表单：直接可用
      return true;
    }
    
    // 组织专属表单：检查组织是否在用户区域运营
    const orgRegions = form.organization.departmentRegions.map(r => r.regionId);
    return orgRegions.includes(userRegionId);
  });
}
```

### 2. ReleaseSnapshot（发布快照）

```prisma
model ReleaseSnapshot {
  id                  String
  regionId            String   // ✅ 保留！区域级版本管理
  formDefinitionId    String
  formVersionId       String
  processVersionId    String?
  status              ReleaseSnapshotStatus
}
```

**regionId 的用途：**

1. **平台级表单**（organizationId = null）
   - 支持不同区域发布不同版本
   - 例如：CN 区域使用 v1.0，US 区域使用 v1.1

2. **组织专属表单**（organizationId != null）
   - regionId 必须是组织运营的区域之一
   - 验证逻辑：`organization.departmentRegions.includes(regionId)`

**发布流程：**

```typescript
async function publishSnapshot(
  formDefinitionId: string,
  formVersionId: string,
  regionId: string
) {
  const form = await prisma.formDefinition.findUnique({
    where: { id: formDefinitionId },
    include: { organization: { include: { departmentRegions: true } } }
  });
  
  // 验证：组织专属表单只能发布到组织运营的区域
  if (form.organizationId) {
    const orgRegions = form.organization.departmentRegions.map(r => r.regionId);
    if (!orgRegions.includes(regionId)) {
      throw new Error(`组织 ${form.organization.name} 未在区域 ${regionId} 运营`);
    }
  }
  
  // 创建发布快照
  return prisma.releaseSnapshot.create({
    data: {
      regionId,
      formDefinitionId,
      formVersionId,
      status: 'ACTIVE'
    }
  });
}
```

### 3. FormInstance（表单实例）

```prisma
model FormInstance {
  id                 String
  formDefinitionId   String
  formVersionId      String
  snapshotId         String?
  businessKey        String
  
  // ✅ 保留！记录提交时的区域环境
  regionId           String?
  
  data               Json
  status             FormInstanceStatus
  createdBy          String
  submittedBy        String?
}
```

**regionId 的用途：**

1. **数据归属标记** - 记录实例创建时的区域环境
2. **区域数据隔离** - 支持按区域查询和统计
3. **审批流路由** - 不同区域可能有不同的审批人
4. **数据分析** - 区域级的表单使用统计

**实例创建：**

```typescript
async function createFormInstance(
  formDefinitionId: string,
  userId: string,
  userRegionId: string,
  data: any
) {
  // 获取该区域的活动快照
  const snapshot = await prisma.releaseSnapshot.findFirst({
    where: {
      formDefinitionId,
      regionId: userRegionId,
      status: 'ACTIVE'
    }
  });
  
  if (!snapshot) {
    throw new Error(`表单未在区域 ${userRegionId} 发布`);
  }
  
  return prisma.formInstance.create({
    data: {
      formDefinitionId,
      formVersionId: snapshot.formVersionId,
      snapshotId: snapshot.id,
      regionId: userRegionId,  // 记录提交时的区域
      businessKey: generateBusinessKey(),
      data,
      status: 'DRAFT',
      createdBy: userId
    }
  });
}
```

## 🎯 设计优势

### 1. **消除冗余**
- ❌ 删除 `regionScope`、`allowedRegions`、`regionApprovalConfig`
- ✅ 区域信息从组织继承，单一数据源

### 2. **逻辑清晰**
```
平台级表单 → 所有组织、所有区域
组织专属表单 → 该组织的运营区域
```

### 3. **灵活的版本管理**
- 通过 `ReleaseSnapshot.regionId` 实现区域级版本控制
- 不同区域可以使用不同版本的表单

### 4. **数据一致性**
- 组织的区域配置是唯一真实来源
- 避免表单区域配置与组织区域不一致的问题

## 🔄 迁移建议

### 现有数据处理

```sql
-- 1. 检查现有数据
SELECT 
  id, 
  name, 
  organization_id,
  region_scope,
  allowed_regions
FROM form_definitions;

-- 2. 平台级表单（organization_id IS NULL）
--    删除区域配置即可，这些表单自动在所有区域可用

-- 3. 组织专属表单（organization_id IS NOT NULL）
--    区域配置从组织继承，删除冗余字段

-- 4. 删除旧字段
ALTER TABLE form_definitions 
DROP COLUMN region_scope,
DROP COLUMN allowed_regions,
DROP COLUMN region_approval_config;
```

### 代码修改清单

**后端 Service：**
- [ ] 删除 `form-management.service.ts` 中的 `regionScope` 相关逻辑
- [ ] 修改查询可用表单的逻辑，从组织获取区域信息
- [ ] 添加发布快照时的组织区域验证

**前端：**
- [ ] 删除区域选择器（已被组织选择器替代）
- [ ] 删除 `FormDefinition` 类型中的区域字段

## 📝 总结

这次架构优化实现了：

1. **简化数据模型** - 删除 3 个冗余字段和 1 个枚举
2. **统一管理** - 通过组织统一管理表单的可用范围
3. **灵活发布** - 保留区域级版本管理能力
4. **清晰逻辑** - 平台级 vs 组织专属，一目了然

核心原则：**组织决定范围，快照管理版本，实例记录环境**。

