b90f901fd9
- feishu_message 表新增 company_id 和 feishu_app_id 字段追踪消息发送者 - 修改 FeishuMessage::saveMessage() 支持保存公司和飞书应用信息 - 修改 FeishuNotificationService 使用原始发送者的飞书应用配置更新消息 - 新增 HttpClient::httpPatchJson() 方法支持 PATCH 请求 - 文档更新:PROJECT_ISSUES.md 添加问题31,APPROVAL_NOTIFICATION_ARCHITECTURE.md 添加多公司架构说明 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
800 lines
30 KiB
Markdown
800 lines
30 KiB
Markdown
# 审批与消息模块架构文档
|
||
|
||
**版本**: 1.0.0
|
||
**更新时间**: 2026-02-28
|
||
**状态**: 设计中
|
||
|
||
---
|
||
|
||
## 📋 概述
|
||
|
||
系统采用**模块化设计**,审批模块和消息模块是两个独立但可联合使用的核心模块。
|
||
|
||
### 设计原则
|
||
|
||
1. **模块独立**:各模块可独立部署和使用
|
||
2. **松耦合**:模块间通过接口通信,不直接依赖
|
||
3. **可扩展**:支持新增业务场景(如请假、采购、报销等)
|
||
4. **统一协议**:所有业务场景遵循相同的审批和消息协议
|
||
|
||
---
|
||
|
||
## 🏗️ 模块架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 业务场景层 │
|
||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||
│ │ 出差审批 │ │ 请假审批 │ │ 采购审批 │ │ 费用报销 │ │
|
||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 审批引擎层(ApprovalService) │
|
||
│ - 流程管理 - 节点管理 - 订单管理 - 记录管理 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 消息服务层(FeishuNotificationService) │
|
||
│ - 消息发送 - 卡片构建 - 消息更新 - 日志记录 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 飞书平台 │
|
||
│ - 消息推送 - 卡片交互 - 回调处理 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 审批模块(Approval Module)
|
||
|
||
### 核心模型
|
||
|
||
| 模型 | 说明 | 关键字段 |
|
||
|------|------|----------|
|
||
| `ApprovalFlow` | 审批流程模板 | business_type, name, status |
|
||
| `ApprovalNode` | 审批节点 | type, approvers, order |
|
||
| `ApprovalOrder` | 审批订单实例 | business_type, business_id, form_data, current_nodes, status |
|
||
| `ApprovalRecord` | 审批操作记录 | order_id, node_id, approver_id, action |
|
||
|
||
### 节点类型
|
||
|
||
```php
|
||
const TYPE_APPROVER = 'approver'; // 单人审批:1人通过即可
|
||
const TYPE_AND_APPROVE = 'and_approve'; // 会签:所有人通过
|
||
const TYPE_OR_APPROVE = 'or_approve'; // 或签:1人通过即可
|
||
const TYPE_NOTIFY = 'notify'; // 通知节点:无需审批
|
||
```
|
||
|
||
### 审批状态流转
|
||
|
||
```
|
||
draft(草稿)
|
||
↓ submit
|
||
pending(审批中)
|
||
↓
|
||
├─→ approved(已通过)
|
||
├─→ rejected(已拒绝)
|
||
└─→ cancelled(已撤销)
|
||
```
|
||
|
||
### 核心方法
|
||
|
||
| 方法 | 说明 |
|
||
|------|------|
|
||
| `submit()` | 提交审批 |
|
||
| `approve()` | 审批通过 |
|
||
| `reject()` | 审批拒绝 |
|
||
| `cancel()` | 撤销申请 |
|
||
| `isNodeCompleted()` | 判断节点是否完成 |
|
||
|
||
### 扩展新业务场景
|
||
|
||
**步骤:**
|
||
|
||
1. **创建业务模型**(如 `LeaveRequest`)
|
||
2. **定义流程类型**(`business_type = 'leave'`)
|
||
3. **实现审批逻辑**
|
||
- 调用 `ApprovalService::submit()` 创建审批订单
|
||
- 在 `form_data` 中存储业务数据
|
||
4. **实现业务逻辑**
|
||
- 审批通过后的业务处理
|
||
- 审批拒绝后的业务处理
|
||
|
||
**示例:**
|
||
```php
|
||
// 请假审批
|
||
$approvalOrder = $approvalService->submit([
|
||
'business_type' => 'leave',
|
||
'business_id' => $leaveRequest->id,
|
||
'title' => '请假申请 - ' . $user->name,
|
||
'applicant_id' => $user->id,
|
||
'flow_id' => $flow->id,
|
||
'form_data' => [
|
||
'reason' => $leaveRequest->reason,
|
||
'start_date' => $leaveRequest->start_date,
|
||
'end_date' => $leaveRequest->end_date,
|
||
'approvers' => $selectedApprovers,
|
||
'cc_users' => $selectedCcUsers,
|
||
],
|
||
]);
|
||
```
|
||
|
||
---
|
||
|
||
## 📨 消息模块(Notification Module)
|
||
|
||
### 核心模型
|
||
|
||
| 模型 | 说明 | 关键字段 |
|
||
|------|------|----------|
|
||
| `FeishuMessage` | 消息映射表 | order_id, node_id, user_id, message_id, status |
|
||
| `FeishuMessageLog` | 消息日志表 | action, order_id, message_id, status, request_data, response_data |
|
||
|
||
### 消息类型
|
||
|
||
| 类型 | 场景 | 卡片样式 |
|
||
|------|------|----------|
|
||
| 审批通知 | 发送给审批人 | 带通过/拒绝按钮 |
|
||
| 系统通知 | 发送给抄送人 | 带查看详情按钮 |
|
||
| 文本通知 | 自审批场景 | 纯文本,无按钮 |
|
||
| 结果通知 | 审批结果通知 | 显示操作结果 |
|
||
|
||
### 核心方法
|
||
|
||
| 方法 | 说明 |
|
||
|------|------|
|
||
| `sendApprovalNotification()` | 发送审批通知(带操作按钮) |
|
||
| `sendSystemNotification()` | 发送系统通知(只读) |
|
||
| `updateMessageCards()` | 更新消息卡片(移除按钮) |
|
||
| `buildApprovalCard()` | 构建审批卡片 |
|
||
| `buildSystemCard()` | 构建系统通知卡片 |
|
||
|
||
### 消息生命周期
|
||
|
||
```
|
||
1. 发送消息
|
||
- 调用飞书 API
|
||
- 获取 message_id
|
||
- 保存映射到 feishu_message
|
||
- 记录日志到 feishu_message_log
|
||
|
||
2. 用户操作
|
||
- 点击卡片按钮
|
||
- 飞书回调处理
|
||
- 执行审批逻辑
|
||
|
||
3. 更新消息(或签/单人场景)
|
||
- 节点完成后触发
|
||
- 更新其他审批人的卡片
|
||
- 移除操作按钮
|
||
- 记录日志
|
||
```
|
||
|
||
### 扩展新消息类型
|
||
|
||
**步骤:**
|
||
|
||
1. **定义卡片构建方法**
|
||
```php
|
||
private function buildXXXCard(array $params): array
|
||
{
|
||
// 返回卡片数据结构
|
||
}
|
||
```
|
||
|
||
2. **添加发送方法**
|
||
```php
|
||
public function sendXXXNotification(array $params): bool
|
||
{
|
||
// 构建卡片
|
||
$card = $this->buildXXXCard($params);
|
||
// 发送消息
|
||
return $this->sendMessage($feishuUserId, $card);
|
||
}
|
||
```
|
||
|
||
3. **添加更新方法**(如需要)
|
||
```php
|
||
public function updateXXXCards(...): bool
|
||
{
|
||
// 更新逻辑
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔗 模块协作
|
||
|
||
### 协作流程
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 业务场景(如出差申请) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────┐
|
||
│ 调用 ApprovalService::submit() │
|
||
│ - 创建审批订单 │
|
||
│ - 返回 approval_order │
|
||
└──────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────┐
|
||
│ 调用 FeishuNotificationService │
|
||
│ - 发送消息给审批人 │
|
||
│ - 保存消息映射 │
|
||
│ - 记录日志 │
|
||
└──────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────┐
|
||
│ 用户点击消息卡片操作 │
|
||
│ - 飞书回调到后端 │
|
||
│ - 调用 ApprovalService::approve() │
|
||
└──────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────┐
|
||
│ 审批完成后 │
|
||
│ - 判断是否需要更新其他人的消息 │
|
||
│ - 或签/单人:调用 updateMessageCards │
|
||
│ - 会签:等待其他人审批 │
|
||
└──────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────┐
|
||
│ 流程结束 │
|
||
│ - 通知申请人结果 │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
### 接口定义
|
||
|
||
**审批模块 → 消息模块**
|
||
|
||
```php
|
||
// 发送审批通知
|
||
$notificationService->sendApprovalNotification([
|
||
'user_id' => $approverId,
|
||
'applicant_id' => $applicantId,
|
||
'title' => '新的审批待办',
|
||
'content' => '...',
|
||
'url' => $detailUrl,
|
||
'order_id' => $orderId,
|
||
'node_id' => $nodeId,
|
||
]);
|
||
|
||
// 更新消息卡片(或签/单人场景)
|
||
$notificationService->updateMessageCards(
|
||
$orderId,
|
||
$nodeId,
|
||
$excludeUserId,
|
||
'approved',
|
||
$operatorName
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 🚧 扩展指南
|
||
|
||
### 添加新的审批业务场景
|
||
|
||
**示例:添加采购审批**
|
||
|
||
1. **创建数据库表**
|
||
```sql
|
||
CREATE TABLE purchase_request (
|
||
id bigint unsigned PRIMARY KEY,
|
||
user_id bigint unsigned NOT NULL,
|
||
reason varchar(500),
|
||
amount decimal(10,2),
|
||
status enum('draft', 'pending', 'approved', 'rejected'),
|
||
approval_order_id bigint unsigned,
|
||
...
|
||
);
|
||
```
|
||
|
||
2. **创建模型**
|
||
```php
|
||
class PurchaseRequest extends Model
|
||
{
|
||
public function approvalOrder()
|
||
{
|
||
return $this->belongsTo(ApprovalOrder::class);
|
||
}
|
||
}
|
||
```
|
||
|
||
3. **创建服务**
|
||
```php
|
||
class PurchaseService
|
||
{
|
||
private $approvalService;
|
||
|
||
public function submit(array $data): PurchaseRequest
|
||
{
|
||
return DB::transaction(function () use ($data) {
|
||
// 创建采购申请
|
||
$purchase = PurchaseRequest::create($data);
|
||
|
||
// 创建审批订单
|
||
$approvalOrder = $this->approvalService->submit([
|
||
'business_type' => 'purchase',
|
||
'business_id' => $purchase->id,
|
||
'title' => '采购申请 - ' . $data['item_name'],
|
||
'applicant_id' => $data['user_id'],
|
||
'flow_id' => $this->getFlowId(),
|
||
'form_data' => [
|
||
'item_name' => $data['item_name'],
|
||
'amount' => $data['amount'],
|
||
'approvers' => $data['approvers'],
|
||
'cc_users' => $data['cc_users'],
|
||
],
|
||
]);
|
||
|
||
// 关联审批订单
|
||
$purchase->approval_order_id = $approvalOrder->id;
|
||
$purchase->status = 'pending';
|
||
$purchase->save();
|
||
|
||
// 发送通知
|
||
$this->sendNotifications($purchase, $approvalOrder);
|
||
|
||
return $purchase;
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
4. **创建控制器**
|
||
```php
|
||
class PurchaseController
|
||
{
|
||
public function submit(Request $request): Response
|
||
{
|
||
$result = $this->service->submit($request->post());
|
||
return $this->success($result);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 添加新的消息类型
|
||
|
||
**示例:添加催办通知**
|
||
|
||
```php
|
||
class FeishuNotificationService
|
||
{
|
||
/**
|
||
* 发送催办通知
|
||
*/
|
||
public function sendReminderNotification(array $params): bool
|
||
{
|
||
$userId = $params['user_id'];
|
||
$card = $this->buildReminderCard($params);
|
||
|
||
// 保存消息映射(如需要)
|
||
if (isset($params['order_id']) && isset($params['node_id'])) {
|
||
\app\model\FeishuMessage::saveMessage(
|
||
$params['order_id'],
|
||
$params['node_id'],
|
||
$userId,
|
||
$messageId
|
||
);
|
||
}
|
||
|
||
return $this->sendMessage($feishuUserId, $card);
|
||
}
|
||
|
||
private function buildReminderCard(array $params): array
|
||
{
|
||
return [
|
||
'header' => [
|
||
'title' => ['content' => '⏰ 审批催办', 'tag' => 'plain_text'],
|
||
'template' => 'orange',
|
||
],
|
||
'elements' => [
|
||
[
|
||
'tag' => 'div',
|
||
'text' => [
|
||
'content' => $params['content'],
|
||
'tag' => 'lark_md',
|
||
],
|
||
],
|
||
[
|
||
'tag' => 'action',
|
||
'actions' => [
|
||
[
|
||
'tag' => 'button',
|
||
'text' => ['content' => '立即处理', 'tag' => 'plain_text'],
|
||
'type' => 'default',
|
||
'url' => $params['url'],
|
||
],
|
||
],
|
||
],
|
||
],
|
||
];
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 数据流向图
|
||
|
||
### 完整审批流程数据流
|
||
|
||
```
|
||
用户提交申请
|
||
↓
|
||
┌───────────────────────────────────────────────────────┐
|
||
│ 1. 业务表(business_trip) │
|
||
│ - 创建业务记录 │
|
||
│ - status = 'draft' │
|
||
└───────────────────────────────────────────────────────┘
|
||
↓
|
||
┌───────────────────────────────────────────────────────┐
|
||
│ 2. 审批订单(approval_order) │
|
||
│ - business_type = 'business_trip' │
|
||
│ - business_id = 业务记录ID │
|
||
│ - form_data = { │
|
||
│ approvers: [用户选择的审批人], │
|
||
│ cc_users: [用户选择的抄送人], │
|
||
│ ...业务数据 │
|
||
│ } │
|
||
│ - current_nodes = [第一个节点ID] │
|
||
│ - status = 'pending' │
|
||
└───────────────────────────────────────────────────────┘
|
||
↓
|
||
┌───────────────────────────────────────────────────────┐
|
||
│ 3. 消息映射(feishu_message) │
|
||
│ - order_id + node_id + user_id → message_id │
|
||
│ - 每个审批人一条记录 │
|
||
└───────────────────────────────────────────────────────┘
|
||
↓
|
||
┌───────────────────────────────────────────────────────┐
|
||
│ 4. 飞书平台 │
|
||
│ - 发送卡片消息给审批人 │
|
||
└───────────────────────────────────────────────────────┘
|
||
↓
|
||
审批人操作
|
||
↓
|
||
┌───────────────────────────────────────────────────────┐
|
||
│ 5. 审批记录(approval_record) │
|
||
│ - order_id, node_id, approver_id │
|
||
│ - action = 'approve' 或 'reject' │
|
||
└───────────────────────────────────────────────────────┘
|
||
↓
|
||
┌───────────────────────────────────────────────────────┐
|
||
│ 6. 节点完成判断 │
|
||
│ - 检查 form_data.approvers(实际选择的审批人) │
|
||
│ - 节点类型决定完成条件 │
|
||
│ - 或签/单人:立即完成,触发消息更新 │
|
||
│ - 会签:等待所有人 │
|
||
└───────────────────────────────────────────────────────┘
|
||
↓
|
||
┌───────────────────────────────────────────────────────┐
|
||
│ 7. 消息更新(或签/单人场景) │
|
||
│ - 更新其他审批人的消息卡片 │
|
||
│ - 移除操作按钮 │
|
||
│ - 标记消息状态 = 'updated' │
|
||
└───────────────────────────────────────────────────────┘
|
||
↓
|
||
┌───────────────────────────────────────────────────────┐
|
||
│ 8. 流程流转 │
|
||
│ - 移动到下一节点 或 完成流程 │
|
||
│ - 通知下一节点审批人 / 通知申请人结果 │
|
||
└───────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 调试与监控
|
||
|
||
### 消息日志查询
|
||
|
||
```bash
|
||
# 查看最近50条日志
|
||
curl "http://localhost:8787/debug/message-logs?debug=1&limit=50"
|
||
|
||
# 查看指定订单的日志
|
||
curl "http://localhost:8787/debug/message-logs?debug=1&order_id=123"
|
||
|
||
# 直接查询数据库
|
||
docker exec 1Panel-mysql-409s mysql -uroot -pmysql_xEsdPa xserver -e "
|
||
SELECT * FROM feishu_message_log
|
||
WHERE order_id = 123
|
||
ORDER BY created_at DESC;
|
||
"
|
||
```
|
||
|
||
### 日志字段说明
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| `action` | send(发送)、update(更新)、update_card(更新卡片) |
|
||
| `status` | success(成功)、failed(失败) |
|
||
| `request_data` | 发送的数据(JSON) |
|
||
| `response_data` | 飞书返回的数据(JSON) |
|
||
| `error_message` | 错误信息(如有) |
|
||
|
||
---
|
||
|
||
## 📝 注意事项
|
||
|
||
1. **审批人来源**:始终从 `form_data.approvers` 获取,不是从 `node.approvers`
|
||
2. **消息更新**:仅在或签/单人审批完成时触发
|
||
3. **防止重复**:审批前检查是否已操作过
|
||
4. **事务处理**:业务逻辑和审批创建在同一事务中
|
||
5. **异常处理**:记录所有失败日志,方便排查问题
|
||
|
||
---
|
||
|
||
## 🏢 多公司多飞书应用架构
|
||
|
||
### 架构概述
|
||
|
||
在多租户场景下,每个公司可以有独立的飞书应用配置:
|
||
- 不同的 `app_id`
|
||
- 不同的 `app_secret`
|
||
- 不同的 bot 身份
|
||
|
||
### 核心约束
|
||
|
||
**飞书API限制**:只有发送消息的bot才能更新该消息。
|
||
|
||
这意味着:
|
||
- 公司A的bot发送的消息,只能由公司A的bot更新
|
||
- 公司B的bot无法更新公司A发送的消息
|
||
- 在跨公司审批流程中,需要追踪每条消息的发送者
|
||
|
||
### 数据模型设计
|
||
|
||
#### feishu_message 表结构
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `id` | bigint | 主键 |
|
||
| `order_id` | bigint | 审批订单ID |
|
||
| `node_id` | bigint | 审批节点ID |
|
||
| `user_id` | bigint | 接收消息的用户ID |
|
||
| `company_id` | bigint | **发送消息的公司ID** |
|
||
| `feishu_app_id` | int | **发送消息的飞书应用ID** |
|
||
| `message_id` | varchar | 飞书消息ID |
|
||
| `msg_token` | varchar | 消息token(用于更新) |
|
||
| `status` | varchar | 消息状态:active/updated/deleted |
|
||
|
||
### 发送消息流程
|
||
|
||
```php
|
||
// FeishuNotificationService::sendApprovalNotification()
|
||
public function sendApprovalNotification(array $params): bool
|
||
{
|
||
$userId = $params['user_id'];
|
||
$orderId = $params['order_id'];
|
||
$nodeId = $params['node_id'];
|
||
|
||
// 1. 获取用户信息(包含公司ID)
|
||
$user = User::findOrFail($userId);
|
||
|
||
// 2. 发送飞书消息
|
||
$response = $this->sendMessage($user->feishu_user_id, $card);
|
||
$messageId = $response['data']['message_id'];
|
||
|
||
// 3. 保存消息映射(包含公司ID和飞书应用ID)
|
||
\app\model\FeishuMessage::saveMessage(
|
||
$orderId,
|
||
$nodeId,
|
||
$userId,
|
||
$messageId,
|
||
null,
|
||
$user->company_id, // ← 记录公司ID
|
||
$this->appId // ← 记录飞书应用ID
|
||
);
|
||
|
||
return true;
|
||
}
|
||
```
|
||
|
||
### 更新消息流程
|
||
|
||
```php
|
||
// FeishuNotificationService::updateMessageCards()
|
||
public function updateMessageCards(
|
||
int $orderId,
|
||
int $nodeId,
|
||
?int $excludeUserId = null,
|
||
string $result = 'approved',
|
||
string $operatorName = ''
|
||
): bool {
|
||
// 1. 获取该订单+节点的所有消息
|
||
$messages = \app\model\FeishuMessage::getMessagesByOrderAndNode($orderId, $nodeId);
|
||
|
||
foreach ($messages as $message) {
|
||
// 2. 获取消息发送时的配置
|
||
$companyId = $message->company_id;
|
||
$feishuAppId = $message->feishu_app_id;
|
||
|
||
if (!$companyId || !$feishuAppId) {
|
||
error_log("❌ 消息记录缺少 company_id 或 feishu_app_id");
|
||
continue;
|
||
}
|
||
|
||
// 3. 临时切换到该消息对应的飞书应用配置
|
||
$originalCompanyId = $this->companyId;
|
||
$originalAppId = $this->appId;
|
||
|
||
$this->companyId = $companyId;
|
||
$this->appId = $feishuAppId;
|
||
|
||
try {
|
||
// 4. 使用正确的飞书应用更新消息
|
||
$response = HttpClient::httpPatchJson(
|
||
"https://open.feishu.cn/open-apis/im/v1/messages/{$message->message_id}",
|
||
['msg_type' => 'interactive', 'content' => $contentJson],
|
||
['content'],
|
||
['Authorization: Bearer ' . $this->getAccessToken()]
|
||
);
|
||
|
||
error_log("✅ 更新消息成功: {$message->message_id}");
|
||
} catch (\Exception $e) {
|
||
error_log("❌ 更新消息失败: {$e->getMessage()}");
|
||
} finally {
|
||
// 5. 恢复原始配置
|
||
$this->companyId = $originalCompanyId;
|
||
$this->appId = $originalAppId;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
```
|
||
|
||
### 跨公司审批场景示例
|
||
|
||
**场景**:公司A的员工发起审批,审批人包括公司A和公司B的员工
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 审批订单创建 │
|
||
│ - 申请人:公司A-用户1 │
|
||
│ - 审批人:[公司A-用户2, 公司B-用户1] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 发送审批通知 │
|
||
│ - 公司A-用户2:使用公司A的飞书应用发送 │
|
||
│ - 公司B-用户1:使用公司B的飞书应用发送 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 公司A-用户2 点击"拒绝" │
|
||
│ - 触发 ApprovalService::reject() │
|
||
│ - 订单状态变为 rejected │
|
||
│ - 需要更新公司B-用户1的消息卡片 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ updateMessageCards() 执行 │
|
||
│ - 查询 feishu_message 表 │
|
||
│ - 公司B-用户1的消息记录: │
|
||
│ · company_id = 公司B的ID │
|
||
│ · feishu_app_id = 公司B的飞书应用ID │
|
||
│ - 切换到公司B的飞书应用配置 │
|
||
│ - 调用 PATCH /messages/{id} 更新消息 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
✅ 成功
|
||
```
|
||
|
||
### 关键实现要点
|
||
|
||
#### 1. FeishuNotificationService 支持动态切换配置
|
||
|
||
```php
|
||
class FeishuNotificationService
|
||
{
|
||
private int $companyId;
|
||
private string $appId;
|
||
private string $appSecret;
|
||
|
||
// 支持运行时切换配置
|
||
public function switchCompany(int $companyId): void
|
||
{
|
||
$this->companyId = $companyId;
|
||
$feishuConfig = $this->getFeishuConfig($companyId);
|
||
$this->appId = $feishuConfig['app_id'];
|
||
$this->appSecret = $feishuConfig['app_secret'];
|
||
}
|
||
|
||
private function getAccessToken(): string
|
||
{
|
||
$cacheKey = "feishu:access_token:{$this->companyId}";
|
||
// 使用 company_id 维度的缓存key
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2. HttpClient 支持预编码JSON字段
|
||
|
||
```php
|
||
// httpPatchJson 方法支持某些字段预编码
|
||
HttpClient::httpPatchJson(
|
||
$url,
|
||
[
|
||
'msg_type' => 'interactive',
|
||
'content' => $preEncodedJson, // 已编码的JSON字符串
|
||
],
|
||
['content'], // 声明 content 字段需要预编码
|
||
$headers
|
||
);
|
||
```
|
||
|
||
#### 3. 数据完整性
|
||
|
||
```sql
|
||
-- 确保所有消息都有 company_id 和 feishu_app_id
|
||
UPDATE feishu_message fm
|
||
JOIN user u ON fm.user_id = u.id
|
||
SET fm.company_id = u.company_id,
|
||
fm.feishu_app_id = (
|
||
SELECT id FROM feishu_config
|
||
WHERE company_id = u.company_id
|
||
LIMIT 1
|
||
)
|
||
WHERE fm.company_id IS NULL;
|
||
```
|
||
|
||
### 常见问题
|
||
|
||
#### Q1: 为什么会出现 "bot or user is not the message's sender" 错误?
|
||
|
||
**A**: 使用了错误的飞书应用配置去更新消息。确保:
|
||
1. `feishu_message` 表正确记录了 `company_id` 和 `feishu_app_id`
|
||
2. 更新消息时使用记录中的配置,而不是当前操作者的配置
|
||
|
||
#### Q2: 如何调试多公司消息更新问题?
|
||
|
||
**A**: 查看日志中的关键信息:
|
||
```bash
|
||
docker logs x-server-backend -f | grep -E "(company_id|feishu_app_id|更新消息)"
|
||
```
|
||
|
||
#### Q3: 历史数据缺少 company_id 怎么办?
|
||
|
||
**A**: 根据用户信息回填:
|
||
```sql
|
||
UPDATE feishu_message fm
|
||
JOIN user u ON fm.user_id = u.id
|
||
SET fm.company_id = u.company_id;
|
||
```
|
||
|
||
### 相关问题文档
|
||
|
||
详见 `docs/PROJECT_ISSUES.md` 第31条:多公司多飞书应用消息卡片更新失败
|
||
|
||
---
|
||
|
||
## 📚 相关文档
|
||
|
||
- [数据库结构文档](./DATABASE_SCHEMA.md)
|
||
- [项目文档](./PROJECT.md)
|
||
- [问题汇总](./PROJECT_ISSUES.md)
|
||
|
||
---
|
||
|
||
**维护人**: 开发团队
|
||
**最后更新**: 2026-03-01
|