# 审批中心 - 架构设计

> **文档版本**: v1.0  
> **创建日期**: 2026-01-02  
> **最后更新**: 2026-01-02  
> **作者**: FFOA 开发团队  
> **状态**: ✅ Approved

---

## 📋 目录

- [1. 架构概览](#1-架构概览)
- [2. 技术栈](#2-技术栈)
- [3. 目录结构](#3-目录结构)
- [4. 核心组件](#4-核心组件)
- [5. 数据流](#5-数据流)
- [6. 状态管理](#6-状态管理)
- [7. API 集成](#7-api-集成)
- [8. 路由设计](#8-路由设计)
- [9. 性能优化](#9-性能优化)

---

## 1. 架构概览

### 1.1 整体架构

```
┌─────────────────────────────────────────────────────────────┐
│                      审批中心 (Frontend)                      │
│                   /approval-center/page.tsx                  │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ HTTP/REST
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                    审批引擎 API (Backend)                     │
│                  /api/v1/approval/*                          │
└─────────────────────────────────────────────────────────────┘
                              │
                   ┌──────────┴──────────┐
                   ↓                     ↓
         ┌─────────────────┐   ┌─────────────────┐
         │  Temporal.io    │   │   PostgreSQL    │
         │  (Workflow)     │   │   (Database)    │
         └─────────────────┘   └─────────────────┘
```

### 1.2 模块边界

**审批中心负责**:
- 📱 前端用户界面和交互
- 🎨 列表展示、详情渲染
- 🔄 状态管理和 UI 更新
- 🚀 用户操作触发 API 调用

**审批引擎负责**:
- ⚙️ 核心审批逻辑
- 🔀 工作流编排（Temporal）
- 💾 数据持久化
- 📢 事件通知

---

## 2. 技术栈

### 2.1 前端框架

| 技术 | 版本 | 用途 |
|-----|------|------|
| **Next.js** | 16.x | React 框架、App Router |
| **React** | 19.x | UI 组件库 |
| **TypeScript** | 5.x | 类型安全 |
| **Tailwind CSS** | 3.x | 样式框架 |
| **shadcn/ui** | Latest | UI 组件库 |
| **Lucide Icons** | Latest | 图标库 |

### 2.2 核心库

| 库 | 用途 |
|----|------|
| `next/navigation` | 路由导航、参数读取 |
| `react-hook-form` | 表单处理 |
| `sonner` | Toast 通知 |
| `date-fns` | 日期格式化 |

### 2.3 自定义 Hooks

| Hook | 文件 | 用途 |
|------|------|------|
| `useAuthGuard` | `@/hooks/use-auth-guard` | 路由权限保护 |
| `useAuth` | `@/contexts/AuthContext` | 用户认证和权限 |
| `useTranslation` | `@/contexts/TranslationContext` | 国际化 |

---

## 3. 目录结构

```
frontend/src/
├── app/
│   ├── (modules)/                      # 业务模块路由组
│   │   └── approval-center/            # 审批中心模块
│   │       ├── page.tsx               # ⭐ 主页面（列表-详情）
│   │       ├── README.md              # 模块说明
│   │       └── submit/                # 表单提交
│   │           └── [formKey]/
│   │               └── page.tsx       # 动态表单提交页
│   │
│   └── (engines)/                      # 引擎路由组
│       └── approvals/
│           └── page.tsx               # 旧路由转发页面
│
├── components/
│   ├── ApprovalTimeline.tsx           # 审批历史时间线
│   ├── FormRenderer.tsx               # 表单渲染器
│   └── UserSelector.tsx               # 用户选择器
│
├── services/
│   └── api/
│       ├── approval.ts                # ⭐ 审批 API 客户端
│       ├── form.ts                    # 表单 API
│       └── organization.ts            # 组织 API
│
├── contexts/
│   ├── AuthContext.tsx                # 认证上下文
│   └── TranslationContext.tsx         # 国际化上下文
│
└── locales/
    ├── approvals/
    │   ├── zh.ts                      # 中文翻译
    │   └── en.ts                      # 英文翻译
    └── ...
```

---

## 4. 核心组件

### 4.1 主页面组件

**文件**: `app/(modules)/approval-center/page.tsx`  
**职责**: 审批中心主界面，包含所有 Tab 和列表-详情布局

```typescript
export default function ApprovalCenterPage() {
  // ========== Hooks ==========
  const router = useRouter();
  const searchParams = useSearchParams();
  const { isReady } = useAuthGuard();
  const { hasPermission, hasRole } = useAuth();
  const { t, locale } = useTranslation();
  
  // ========== State ==========
  const [activeTab, setActiveTab] = useState<TabType>('submit');
  const [items, setItems] = useState<ApprovalItem[]>([]);
  const [selectedItem, setSelectedItem] = useState<ApprovalItem | null>(null);
  const [detail, setDetail] = useState<ApprovalDetail | null>(null);
  const [loading, setLoading] = useState(true);
  
  // ========== Effects ==========
  useEffect(() => {
    loadItems(); // 加载列表
  }, [activeTab]);
  
  useEffect(() => {
    if (selectedItem) {
      loadDetail(selectedItem.id); // 加载详情
    }
  }, [selectedItem]);
  
  // ========== Handlers ==========
  const handleApprove = async () => { /* ... */ };
  const handleReject = async () => { /* ... */ };
  const handleWithdraw = async () => { /* ... */ };
  const handleForward = async () => { /* ... */ };
  
  // ========== Render ==========
  return (
    <div>
      {renderTabs()}
      <div className="flex">
        {renderList()}
        {renderDetailPanel()}
      </div>
    </div>
  );
}
```

**关键特性**:
- 单一组件管理所有 Tab（避免多页面切换）
- 列表-详情分栏布局
- 实时数量更新（Tab badge）
- 深度链接支持（instanceId 参数）

### 4.2 ApprovalTimeline 组件

**文件**: `components/ApprovalTimeline.tsx`  
**职责**: 渲染审批历史时间线

```typescript
interface ApprovalTimelineProps {
  history: ApprovalNode[];
  locale?: 'zh' | 'en';
}

export const ApprovalTimeline: React.FC<ApprovalTimelineProps> = ({
  history,
  locale = 'zh'
}) => {
  // ⭐ 核心设计：过滤掉结束节点（EndNode）
  // 参考钉钉/飞书：最后一个实际操作就是终点
  const filteredHistory = history.filter(node => {
    const isEndNode = 
      node.nodeType === 'END_NODE' ||
      node.nodeType === 'PROCESS_END' ||
      node.nodeName === '流程结束' ||
      node.nodeName === '结束';
    
    return !isEndNode; // 不展示结束节点
  });

  return (
    <div className="space-y-6 pb-8">
      {filteredHistory.map((node, nodeIndex) => {
        const isLastNode = nodeIndex === filteredHistory.length - 1;
        
        return (
          <div key={node.nodeId}>
            {/* 节点标题 */}
            <h4>{node.nodeName}</h4>
            
            {/* 任务列表 */}
            {node.tasks.map((task, taskIndex) => {
              const isLastTask = taskIndex === node.tasks.length - 1;
              
              return (
                <div key={task.id}>
                  {/* 审批人 */}
                  {task.assignee && <div>{task.assignee.name}</div>}
                  
                  {/* 操作记录 */}
                  {task.actions.map((action, actionIndex) => {
                    const isLastAction = actionIndex === task.actions.length - 1;
                    const isVeryLastAction = isLastNode && isLastTask && isLastAction;
                    
                    return (
                      <div key={action.id}>
                        {getActionIcon(action.action)}
                        {getActionText(action.action, locale)}
                        {action.comment && <p>{action.comment}</p>}
                        
                        {/* ⭐ 最后一个操作添加结束分隔线 */}
                        {isVeryLastAction && (
                          <div className="mt-4 pt-3 border-t border-gray-200">
                            <div className="text-xs text-gray-400">
                              流程结束于 {formatDateTime(action.actionTime)}
                            </div>
                          </div>
                        )}
                      </div>
                    );
                  })}
                </div>
              );
            })}
          </div>
        );
      })}
    </div>
  );
};
```

**关键特性**:
- ✅ **过滤结束节点**: 不展示 `END_NODE` 类型节点
- 🎨 **视觉优化**: 最后一个操作添加结束分隔线
- 📏 **底部空白**: 容器添加 `pb-8` 提供呼吸空间
- 🔄 **支持转交**: 显示"XXX转交给YYY"
- 👤 **智能显示操作人**: 避免重复显示审批人和操作人
- 🎯 **管理员操作**: 特殊图标和文案

**设计原则**:
> **参考钉钉/飞书**：最后一个实际操作就是流程的终点，不需要单独的"结束"节点。整体状态通过详情头部的状态标签展示。

### 4.3 FormRenderer 组件

**文件**: `components/FormRenderer.tsx`（表单引擎提供）  
**职责**: 通用表单数据渲染器

```typescript
interface FormRendererProps {
  schema: FormSchema;        // JSON Schema
  uiSchema: UiSchema;        // UI Schema
  formData: any;             // 表单数据
  onChange?: (data: any) => void;
  disabled?: boolean;        // 只读模式
  showSaveButton?: boolean;
  showSubmitButton?: boolean;
}

// 审批中心使用方式（只读）
<FormRenderer
  schema={detail.formSchema}
  uiSchema={detail.formUiSchema || {}}
  formData={detail.formData}
  onChange={() => {}}
  disabled={true}
  showSaveButton={false}
  showSubmitButton={false}
  showSubmitterInfo={false}
/>
```

### 4.4 UserSelector 组件

**文件**: `components/UserSelector.tsx`  
**职责**: 用户选择器（转交、加签等）

```typescript
interface UserSelectorProps {
  onSelect: (user: UserInfo) => void;
  placeholder?: string;
  filterOutCurrentUser?: boolean;
}
```

---

## 5. 数据流

### 5.1 列表加载流程

```
用户切换 Tab
   ↓
setActiveTab(newTab)
   ↓
useEffect 触发 loadItems()
   ↓
根据 activeTab 调用不同 API:
  - submit:    getFormDefinitions()
  - pending:   getMyPendingTasks()
  - initiated: getMyInitiatedProcesses()
  - processed: getMyProcessedTasks()
  - cc:        getMyCCTasks()
  - admin:     getAdminInstances()
   ↓
mapXXXToItems() 统一数据格式
   ↓
setItems(mappedItems)
   ↓
UI 重新渲染列表
```

### 5.2 详情加载流程

```
用户点击列表项
   ↓
setSelectedItem(item)
   ↓
useEffect 触发 loadDetail(item.id)
   ↓
并行请求:
  - getApprovalHistory(instanceId)  → 审批历史
  - getFormInstance(businessId)     → 表单数据
   ↓
setDetail({
  history: approvalHistory,
  formSchema, formData, ...
})
   ↓
UI 重新渲染详情面板
```

### Temporal 双写 + 补偿事务模式

- Service 层立即更新数据库任务状态（前端可立即查询最新状态）
- 同时发送 signal 给 Temporal workflow
- Signal 失败时，补偿事务回滚任务状态
- Temporal workflow 保持幂等，重复 signal 不冲突
- 选择理由：前端无延迟，最终一致性通过补偿保证

### 5.3 审批操作流程

```
用户点击"通过"按钮
   ↓
setApproving(true)
   ↓
approveTask({
  instanceId,
  taskId,
  comment,
  ...
})
   ↓
后端处理:
  - 更新 approval_tasks 状态
  - Temporal workflow 推进流程
  - 创建 action_log
   ↓
前端响应:
  - setApproving(false)
  - toast.success('审批成功')
  - loadItems()          // 刷新列表
  - loadAllTabCounts()   // 刷新数量
  - setSelectedItem(null) // 清空选中项
   ↓
延迟刷新 (800ms):
  - loadItems()          // 二次刷新确保数据一致
  - loadAllTabCounts()
```

---

## 6. 状态管理

### 6.1 状态结构

```typescript
// Tab 状态
const [activeTab, setActiveTab] = useState<TabType>('submit');

// 列表数据
const [items, setItems] = useState<ApprovalItem[]>([]);
const [selectedItem, setSelectedItem] = useState<ApprovalItem | null>(null);
const [loading, setLoading] = useState(true);

// 详情数据
const [detail, setDetail] = useState<ApprovalDetail | null>(null);
const [detailLoading, setDetailLoading] = useState(false);

// 操作状态
const [approving, setApproving] = useState(false);
const [rejecting, setRejecting] = useState(false);
const [comment, setComment] = useState('');

// 转交状态
const [forwarding, setForwarding] = useState(false);
const [forwardDialogOpen, setForwardDialogOpen] = useState(false);
const [forwardComment, setForwardComment] = useState('');
const [forwardTargetUser, setForwardTargetUser] = useState<UserInfo | null>(null);

// 撤回状态
const [withdrawing, setWithdrawing] = useState(false);
const [withdrawDialogOpen, setWithdrawDialogOpen] = useState(false);
const [withdrawReason, setWithdrawReason] = useState('');

// 管理员操作状态
const [adminActionType, setAdminActionType] = useState<string>('');
const [adminReason, setAdminReason] = useState('');
const [adminComment, setAdminComment] = useState('');
const [adminProcessing, setAdminProcessing] = useState(false);
const [adminTargetUser, setAdminTargetUser] = useState<UserInfo | null>(null);

// 数量统计
const [tabCounts, setTabCounts] = useState({
  pending: 0,
  initiated: 0,
  processed: 0,
  cc: 0,
  admin: 0,
});
```

### 6.2 状态更新策略

**原则**:
- 最小化状态（避免冗余）
- 单一数据源（避免不一致）
- 及时清理（操作后重置状态）

**示例**:
```typescript
// ✅ 正确：操作后清理状态
const handleApprove = async () => {
  setApproving(true);
  try {
    await approveTask(...);
    toast.success('审批成功');
    
    // 清理状态
    setComment('');
    setSelectedItem(null);
    setDetail(null);
    
    // 刷新数据
    await loadItems();
    await loadAllTabCounts();
  } finally {
    setApproving(false);
  }
};
```

---

## 7. API 集成

### 7.1 API 客户端

**文件**: `services/api/approval.ts`

```typescript
// 待办任务
export async function getMyPendingTasks(params?: PendingQuery): Promise<PaginatedResponse<ApprovalTaskItem>> {
  return apiClient.get('/approval/my-tasks/pending', { params });
}

// 我发起的
export async function getMyInitiatedProcesses(params?: InitiatedQuery): Promise<PaginatedResponse<ProcessInstanceItem>> {
  return apiClient.get('/approval/my-processes/initiated', { params });
}

// 我已处理
export async function getMyProcessedTasks(params?: ProcessedQuery): Promise<PaginatedResponse<ApprovalTaskItem>> {
  return apiClient.get('/approval/my-tasks/processed', { params });
}

// 审批操作
export async function approveTask(data: ApproveRequest): Promise<ApprovalResponse> {
  return apiClient.post('/approval/tasks/approve', data);
}

export async function rejectTask(data: RejectRequest): Promise<ApprovalResponse> {
  return apiClient.post('/approval/tasks/reject', data);
}

// 撤回
export async function withdrawProcess(instanceId: string, reason: string): Promise<void> {
  return apiClient.post(`/approval/processes/${instanceId}/withdraw`, { reason });
}

// 转交
export async function forwardTask(data: ForwardRequest): Promise<void> {
  return apiClient.post('/approval/tasks/forward', data);
}

// 管理员操作
export async function adminTerminateProcess(data: AdminTerminateRequest): Promise<void> {
  return apiClient.post('/approval/admin/terminate', data);
}

export async function adminApproveTask(data: AdminApproveRequest): Promise<void> {
  return apiClient.post('/approval/admin/approve', data);
}

// 管理员数据中心
export async function getAdminAnalytics(params: AdminAnalyticsQuery) {
  return apiClient.get('/approval/admin/analytics', { params });
}

export async function getAdminInstances(params: AdminInstancesQuery) {
  return apiClient.get('/approval/admin/instances', { params });
}

export async function exportAdminInstances(params: AdminInstancesQuery) {
  return apiClient.post('/approval/admin/instances/export', params);
}

export async function getAdminExports() {
  return apiClient.get('/approval/admin/exports');
}

export async function getAdminExportStatus(taskId: string) {
  return apiClient.get(`/approval/admin/exports/${taskId}`);
}

export async function getAdminSettings() {
  return apiClient.get('/approval/admin/settings');
}

export async function updateAdminSettings(data: { exportRetentionDays: number }) {
  return apiClient.put('/approval/admin/settings', data);
}
```

### 7.2 错误处理

```typescript
try {
  const result = await approveTask(...);
  toast.success('审批成功');
} catch (error) {
  if (error instanceof ApiClientError) {
    // 业务错误
    toast.error(error.message);
  } else {
    // 网络错误
    toast.error('操作失败，请稍后重试');
  }
  console.error('Approve error:', error);
}
```

### 7.3 API 调用时机

| 时机 | API | 说明 |
|-----|-----|------|
| Tab 切换 | `getXXX()` | 加载对应列表 |
| 列表项点击 | `getApprovalHistory()` | 加载详情 |
| 审批操作 | `approveTask()` / `rejectTask()` | 提交审批 |
| 撤回操作 | `withdrawProcess()` | 撤回流程 |
| 转交操作 | `forwardTask()` | 转交任务 |
| 管理员操作 | `adminXXX()` | 管理员干预 |
| 页面加载 | `getFormDefinitions()` | 获取表单列表（submit tab） |
| 操作后 | `getXXX()` + `getXXXCount()` | 刷新列表和数量 |

---

## 8. 路由设计

### 8.1 路由结构

```
/approval-center                       → 主页面
  ?tab=submit                          → 提交请求
  ?tab=pending                         → 待我审批
  ?tab=initiated                       → 我提交的
  ?tab=processed                       → 我审批的
  ?tab=cc                              → 抄送我的
  ?tab=admin                           → 管理员处理
  ?tab=pending&instanceId=xxx          → 深度链接

/approval-center/submit/{formKey}      → 表单提交

/approvals                             → 旧路由（自动转发）
```

### 8.2 路由实现

**主页面**:
```typescript
'use client';

export default function ApprovalCenterPage() {
  const searchParams = useSearchParams();
  const tabFromUrl = searchParams.get('tab') as TabType | null;
  const instanceIdFromUrl = searchParams.get('instanceId');
  
  // 设置默认 Tab
  const defaultTab = (tabFromUrl && validTabs.includes(tabFromUrl)) ? tabFromUrl : 'submit';
  const [activeTab, setActiveTab] = useState<TabType>(defaultTab);
  
  // 深度链接：自动选中指定的审批项
  useEffect(() => {
    if (instanceIdFromUrl && items.length > 0) {
      const item = items.find(i => i.id === instanceIdFromUrl);
      if (item) {
        setSelectedItem(item);
      }
    }
  }, [instanceIdFromUrl, items]);
}
```

**转发页面**:
```typescript
'use client';

export default function ApprovalsRedirect() {
  const router = useRouter();
  const searchParams = useSearchParams();

  useEffect(() => {
    const params = searchParams.toString();
    const newPath = params ? `/approval-center?${params}` : '/approval-center';
    router.replace(newPath);
  }, [router, searchParams]);

  return <div>正在跳转...</div>;
}
```

### 8.3 路由更新

**切换 Tab**:
```typescript
const handleTabChange = (tab: TabType) => {
  setActiveTab(tab);
  // 可选：更新 URL（不刷新页面）
  router.push(`/approval-center?tab=${tab}`, { scroll: false });
};
```

---

## 9. 性能优化

### 9.1 列表优化

**分页加载**:
```typescript
const loadItems = async () => {
  const result = await getMyPendingTasks({
    page: 1,
    limit: 20, // 每页20条
  });
  setItems(result.items);
};
```

**虚拟滚动**（未来优化）:
- 当列表超过 100 条时，使用 `react-window` 虚拟化

### 9.2 详情优化

**并行加载**:
```typescript
const loadDetail = async (instanceId: string) => {
  setDetailLoading(true);
  try {
    // 并行请求
    const [history, formInstance] = await Promise.all([
      getApprovalHistory(instanceId),
      getFormInstance(businessId),
    ]);
    setDetail({ history, formData, ... });
  } finally {
    setDetailLoading(false);
  }
};
```

### 9.3 防抖和节流

**搜索防抖**:
```typescript
const [searchTerm, setSearchTerm] = useState('');

const debouncedSearch = useMemo(
  () => debounce((term: string) => {
    // 执行搜索
    loadItems({ search: term });
  }, 300),
  []
);

useEffect(() => {
  debouncedSearch(searchTerm);
}, [searchTerm, debouncedSearch]);
```

### 9.4 缓存策略

**Tab 切换缓存**（未来优化）:
```typescript
const [cachedItems, setCachedItems] = useState<Record<TabType, ApprovalItem[]>>({});

const loadItems = async () => {
  // 检查缓存
  if (cachedItems[activeTab]) {
    setItems(cachedItems[activeTab]);
    return;
  }
  
  // 加载数据
  const result = await getXXX();
  
  // 更新缓存
  setCachedItems(prev => ({
    ...prev,
    [activeTab]: result.items,
  }));
  setItems(result.items);
};
```

---

## 10. 安全性

### 10.1 认证

```typescript
// 页面级别保护
const { isReady } = useAuthGuard();

if (!isReady) {
  return <div>Loading...</div>;
}
```

### 10.2 权限控制

```typescript
// 管理员 Tab 权限
const { hasRole } = useAuth();
const isAdmin = hasRole('Administrator');

if (isAdmin) {
  tabs.push({ key: 'admin', ... });
}
```

### 10.3 操作验证

```typescript
// 后端验证所有操作
// frontend 只负责 UI，不做业务判断
```

---

## 附录 A: 类型定义

```typescript
// Tab 类型
type TabType = 'submit' | 'pending' | 'initiated' | 'processed' | 'cc' | 'admin';

// 审批项（列表）
interface ApprovalItem {
  id: string;
  title: string;
  businessType: string;
  formKey: string;
  submitter: {
    id: string;
    name: string;
    avatar?: string;
  };
  submitTime: string;
  status: InstanceStatus;
  currentNode: string;
  raw: any; // 原始数据
}

// 审批详情
interface ApprovalDetail {
  id: string;
  title: string;
  formSchema: FormSchema;
  formUiSchema: UiSchema;
  formData: any;
  history: ApprovalNode[];
  status: InstanceStatus;
}

// 审批节点
interface ApprovalNode {
  nodeId: string;
  nodeName: string;
  nodeType: string;
  status: NodeStatus;
  startTime: string;
  tasks: ApprovalTask[];
}

// 审批任务
interface ApprovalTask {
  id: string;
  assignee: UserInfo;
  status: TaskStatus;
  actions: ApprovalAction[];
  autoApproved?: boolean;
  autoApproveReason?: string;
}

// 审批操作
interface ApprovalAction {
  id: string;
  action: ActionType;
  operator: UserInfo;
  targetUser?: UserInfo; // 转交目标用户
  comment?: string;
  actionTime: string;
}

// 实例状态（新枚举）
type InstanceStatus = 
  | 'RUNNING'      // 审批中
  | 'SUSPENDED'    // 已暂停
  | 'APPROVED'     // 已通过
  | 'REJECTED'     // 已拒绝
  | 'WITHDRAWN'    // 已撤回
  | 'TERMINATED'   // 已终止
  | 'FAILED';      // 失败
```

---

**文档结束**

**维护者**: FFOA 开发团队  
**最后更新**: 2026-01-02  
**版本**: v1.0
