diff --git a/docs/TEST_REPORT_APPROVAL_20260301.md b/docs/TEST_REPORT_APPROVAL_20260301.md new file mode 100644 index 0000000..e3d67e1 --- /dev/null +++ b/docs/TEST_REPORT_APPROVAL_20260301.md @@ -0,0 +1,371 @@ +# 审批流程测试报告 + +**测试日期**: 2026-03-01 +**测试版本**: feature/debug-feishu-message (commit: 1e2d337) +**测试环境**: 生产环境 (45.153.130.80) +**测试人员**: Claude + +--- + +## 📋 测试环境配置 + +### 服务器信息 +``` +IP: 45.153.130.80 +域名: uniservice-mobile.hnxinyouhe.com +后端: x-server-backend-prod (PHP 8.5.3) +数据库: 1Panel-mysql-XyxS (MySQL) +``` + +### 节点配置 +``` +节点ID: 7 +节点名称: 李佳博 +节点类型: and_approve (会签) +所属流程: 2 (出差审批) +节点顺序: 0 +``` + +### 测试用户 +``` +申请人: 李佳博 (ID: 1) +审批人: + - 陈兰 (ID: 2) + - 陈标 (ID: 3) +``` + +--- + +## 🧪 测试场景 + +### 场景1: 会签流程 - 所有人通过 + +#### 流程图 +``` +提交申请 → 审批人1通过 → 审批人2通过 → 订单完成 + ↓ ↓(pending) ↓(approved) + pending 1/2 已通过 2/2 已通过 +``` + +#### 测试步骤 + +| 步骤 | 操作 | 参数 | 预期结果 | 实际结果 | 状态 | +|------|------|------|----------|----------|------| +| 1 | 创建审批订单 | `form_data.approvers = [2, 3]`
`current_nodes = [7]`
`status = "pending"` | 订单ID创建成功 | 订单ID=14 | ✅ | +| 2 | 审批人1通过 | `order_id = 14`
`approver_id = 2`
`action = "approve"` | 状态=pending
已审批=1/2 | 状态=pending
已审批=1/2 | ✅ | +| 3 | 审批人2通过 | `order_id = 14`
`approver_id = 3`
`action = "approve"` | 状态=approved
已审批=2/2 | 状态=approved
已审批=2/2 | ✅ | +| 4 | 验证审批记录 | `order_id = 14`
`action = "approve"` | 2条记录 | 2条记录 | ✅ | + +#### 审批记录 +``` +ID 1: 陈兰 (ID:2) 在节点 7 审批通过 +ID 2: 陈标 (ID:3) 在节点 7 审批通过 +``` + +#### 测试代码 +```php +// 创建订单 +$order = ApprovalOrder::create([ + 'business_type' => 'business_trip', + 'business_id' => 100, + 'title' => '会签测试', + 'applicant_id' => 1, + 'status' => 'pending', + 'current_nodes' => [7], + 'flow_id' => 2, + 'form_data' => ['approvers' => [2, 3]], +]); + +// 审批人1通过 +$service->approve(14, 2); + +// 审批人2通过 +$service->approve(14, 3); +``` + +--- + +### 场景2: 拒绝流程 - 任一人拒绝 + +#### 流程图 +``` +提交申请 → 审批人1拒绝 → 订单结束 + ↓ ↓(rejected) + pending 流程终止 +``` + +#### 测试步骤 + +| 步骤 | 操作 | 参数 | 预期结果 | 实际结果 | 状态 | +|------|------|------|----------|----------|------| +| 1 | 创建审批订单 | `form_data.approvers = [2, 3]`
`status = "pending"` | 订单ID创建成功 | 订单ID=15 | ✅ | +| 2 | 审批人1拒绝 | `order_id = 15`
`approver_id = 2`
`action = "reject"`
`comment = "测试拒绝"` | 状态=rejected | 状态=rejected | ✅ | +| 3 | 验证审批记录 | `order_id = 15`
`action = "reject"` | 1条拒绝记录 | 1条拒绝记录 | ✅ | + +#### 审批记录 +``` +ID 1: 陈兰 (ID:2) 在节点 7 审批拒绝 - 原因: "测试拒绝" +``` + +#### 测试代码 +```php +$order = ApprovalOrder::create([ + 'business_type' => 'business_trip', + 'business_id' => 101, + 'title' => '拒绝测试', + 'applicant_id' => 1, + 'status' => 'pending', + 'current_nodes' => [7], + 'flow_id' => 2, + 'form_data' => ['approvers' => [2, 3]], +]); + +// 审批人1拒绝 +$service->reject(15, 2, '测试拒绝'); +``` + +--- + +### 场景3: 重复操作防护 - 同一人不能重复审批 + +#### 流程图 +``` +提交申请 → 审批人1通过 → 审批人1再次通过 + ↓ ↓(success) ↓(异常) + pending 记录已创建 抛出异常 +``` + +#### 测试步骤 + +| 步骤 | 操作 | 参数 | 预期结果 | 实际结果 | 状态 | +|------|------|------|----------|----------|------| +| 1 | 创建审批订单 | `form_data.approvers = [2, 3]`
`status = "pending"` | 订单ID创建成功 | 订单ID=16 | ✅ | +| 2 | 审批人1第一次通过 | `order_id = 16`
`approver_id = 2` | 操作成功 | 操作成功 | ✅ | +| 3 | 审批人1第二次通过 | `order_id = 16`
`approver_id = 2` | 抛出异常 | "您已经通过过此申请" | ✅ | + +#### 异常信息 +``` +Exception: 您已经通过过此申请,无需重复操作 +``` + +#### 测试代码 +```php +$order = ApprovalOrder::create([ + 'business_type' => 'business_trip', + 'business_id' => 102, + 'title' => '重复操作测试', + 'applicant_id' => 1, + 'status' => 'pending', + 'current_nodes' => [7], + 'flow_id' => 2, + 'form_data' => ['approvers' => [2, 3]], +]); + +try { + $service->approve(16, 2); // 第一次 - 成功 + $service->approve(16, 2); // 第二次 - 异常 +} catch (Exception $e) { + // 捕获重复操作异常 +} +``` + +--- + +### 场景4: 撤销流程 - 申请人可撤销 + +#### 流程图 +``` +提交申请 → 申请人撤销 → 订单结束 + ↓ ↓(cancelled) + pending 流程终止 +``` + +#### 测试步骤 + +| 步骤 | 操作 | 参数 | 预期结果 | 实际结果 | 状态 | +|------|------|------|----------|----------|------| +| 1 | 创建审批订单 | `applicant_id = 1`
`status = "pending"` | 订单ID创建成功 | 订单ID=17 | ✅ | +| 2 | 申请人撤销 | `order_id = 17`
`user_id = 1` | 状态=cancelled | 状态=cancelled | ✅ | +| 3 | 验证撤销记录 | `order_id = 17`
`action = "cancel"` | 1条撤销记录 | 1条撤销记录 | ✅ | + +#### 审批记录 +``` +ID 1: 李佳博 (ID:1) 撤销了申请 +``` + +#### 测试代码 +```php +$order = ApprovalOrder::create([ + 'business_type' => 'business_trip', + 'business_id' => 103, + 'title' => '撤销测试', + 'applicant_id' => 1, + 'status' => 'pending', + 'current_nodes' => [7], + 'flow_id' => 2, + 'form_data' => ['approvers' => [2, 3]], +]); + +// 申请人撤销 +$service->cancel(17, 1); +``` + +--- + +## 📊 核心逻辑验证 + +### 节点完成判断逻辑 + +**代码位置**: `ApprovalService::isNodeCompleted()` + +```php +private function isNodeCompleted(int $orderId, int $nodeId): bool +{ + $node = ApprovalNode::findOrFail($nodeId); + $records = ApprovalRecord::where('order_id', $orderId) + ->where('node_id', $nodeId) + ->where('action', 'approve') + ->get(); + + // 从 form_data 获取实际审批人列表 + $order = ApprovalOrder::find($orderId); + $formData = $order->form_data ?? []; + $approvers = $formData['approvers'] ?? []; + + if (!empty($approvers) && is_array($approvers[0] ?? null) && isset($approvers[0]['id'])) { + $approverIds = array_column($approvers, 'id'); + } else { + $approverIds = $approvers; + } + + if ($node->type === ApprovalNode::TYPE_AND_APPROVE) { + // 会签:需要所有人通过 + return count($records) >= count($approverIds); + } elseif ($node->type === ApprovalNode::TYPE_OR_APPROVE) { + // 或签:任一人通过即可 + return count($records) >= 1; + } else { + // 单人审批:需要一人通过 + return count($records) >= 1; + } +} +``` + +**验证结果**: ✅ 通过 + +| 节点类型 | 完成条件 | 测试结果 | +|----------|----------|----------| +| and_approve (会签) | 已审批人数 >= 总审批人数 | ✅ | +| or_approve (或签) | 已审批人数 >= 1 | ⏳ 未测试 | +| approver (单人) | 已审批人数 >= 1 | ⏳ 未测试 | + +--- + +## 🔔 消息发送功能 + +### 当前状态 +- ⚠️ **飞书消息发送失败** (emoji 编码问题) + +### 错误信息 +``` +ErrCode: 200621; +ErrMsg: parse card json err, please check whether the card json is correct +``` + +### 原因分析 +PHP 的 `json_encode()` 默认将 emoji 编码成 Unicode 转义序列: +- `🔔` → `\ud83d\udd14` +- `📋` → `\ud83d\udccb` + +飞书 API 不接受这种编码格式。 + +### 待修复项 +1. 修改 `FeishuNotificationService` 的 JSON 编码选项 +2. 使用 `JSON_UNESCAPED_UNICODE` 标志 + +--- + +## 🗄️ 数据库表状态 + +### feishu_message_log +```sql +CREATE TABLE IF NOT EXISTS feishu_message_log ( + id bigint unsigned NOT NULL AUTO_INCREMENT, + action varchar(20) NOT NULL COMMENT '操作类型:send=发送, update=更新, update_card=更新卡片', + order_id bigint unsigned NOT NULL COMMENT '审批订单ID', + node_id bigint unsigned DEFAULT NULL COMMENT '审批节点ID', + user_id bigint unsigned DEFAULT NULL COMMENT '用户ID', + message_id varchar(64) DEFAULT NULL COMMENT '飞书消息ID', + status varchar(10) NOT NULL COMMENT '状态:success=成功, failed=失败', + request_data text DEFAULT NULL COMMENT '请求数据(JSON)', + response_data text DEFAULT NULL COMMENT '响应数据(JSON)', + error_message text DEFAULT NULL COMMENT '错误信息', + created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_order_id (order_id), + KEY idx_action (action), + KEY idx_status (status), + KEY idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='飞书消息日志表'; +``` + +**状态**: ✅ 已创建并包含 `updated_at` 字段 + +--- + +## ✅ 测试结论 + +### 通过的测试 +1. ✅ 会签流程 - 所有人通过后订单完成 +2. ✅ 拒绝流程 - 任一人拒绝后订单结束 +3. ✅ 重复操作防护 - 同一人不能重复审批 +4. ✅ 撤销流程 - 申请人可撤销申请 +5. ✅ 审批记录正确保存 +6. ✅ 节点完成判断逻辑正确 + +### 待解决的问题 +1. ⚠️ 飞书消息发送失败 (emoji 编码问题) +2. ⏳ 或签 (or_approve) 场景未测试 +3. ⏳ 单人审批 (approver) 场景未测试 +4. ⏳ 多节点流转未测试 + +### 建议 +1. 优先修复飞书消息 emoji 编码问题 +2. 补充或签和单人审批场景测试 +3. 测试多节点流转场景 +4. 测试消息卡片更新功能(或签/单人审批完成后) + +--- + +## 📝 附录 + +### 测试文件位置 +``` +/www/x-server-mobile/docs/TEST_REPORT_APPROVAL_20260301.md +``` + +### 相关代码文件 +``` +backend/app/service/ApprovalService.php # 审批服务 +backend/app/service/FeishuNotificationService.php # 飞书通知服务 +backend/app/model/ApprovalOrder.php # 审批订单模型 +backend/app/model/ApprovalRecord.php # 审批记录模型 +backend/app/model/ApprovalNode.php # 审批节点模型 +backend/app/model/FeishuMessageLog.php # 消息日志模型 +``` + +### 测试执行命令 +```bash +# 会签流程测试 +docker exec x-server-backend-prod php /app/test_approval_logic.php + +# 全场景测试 +docker exec x-server-backend-prod php /app/test_approval_all.php +``` + +--- + +**报告生成时间**: 2026-03-01 09:18:00 +**测试执行者**: Claude Code +**审核状态**: 待审核