Files
x-server-mobile/backend/app/service/ApprovalService.php
T
root 1f72da9ec4 feat: 添加消息卡片更新功能,支持或签/单人审批场景
新增功能:
1. 创建 feishu_message 表,存储消息与审批订单/节点/用户的映射关系
2. 发送审批通知时保存 message_id
3. 审批完成后自动更新其他审批人的消息卡片

使用场景:
- 或签(or_approve):一人通过后,更新其他审批人的消息卡片
- 单人审批(approver):一人通过后,更新其他审批人的消息卡片
- 拒绝(reject):拒绝后更新所有其他审批人的消息卡片

技术细节:
- FeishuMessage 模型:管理消息映射
- FeishuNotificationService::updateMessageCards():更新消息卡片
- ApprovalService:审批完成后调用消息更新逻辑

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 18:06:40 +08:00

677 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\service;
use app\model\ApprovalFlow;
use app\model\ApprovalNode;
use app\model\ApprovalOrder;
use app\model\ApprovalRecord;
use app\model\User;
use support\Db;
use Illuminate\Database\Connection;
class ApprovalService
{
private Connection $db;
private ?FeishuNotificationService $notificationService = null;
public function __construct()
{
$this->db = Db::connection();
}
/**
* 提交审批
*/
public function submit(array $data): ApprovalOrder
{
return $this->db->transaction(function () use ($data) {
// 1. 获取审批流程
$flow = ApprovalFlow::active()
->byBusinessType($data['business_type'])
->with('nodes')
->findOrFail($data['flow_id']);
// 2. 创建审批单
$order = ApprovalOrder::create([
'business_type' => $data['business_type'],
'business_id' => $data['business_id'],
'title' => $data['title'],
'applicant_id' => $data['applicant_id'],
'status' => ApprovalOrder::STATUS_PENDING,
'current_nodes' => $this->getFirstNodeIds($flow),
'flow_id' => $flow->id,
'form_data' => $data['form_data'] ?? [],
'attachments' => $data['attachments'] ?? [],
]);
// 3. 记录提交操作
ApprovalRecord::create([
'order_id' => $order->id,
'node_id' => $flow->nodes->first()->id ?? 0,
'node_type' => 'submit',
'approver_id' => $data['applicant_id'],
'action' => ApprovalRecord::ACTION_SUBMIT,
'comment' => $data['comment'] ?? null,
]);
return $order->load(['flow', 'applicant']);
});
}
/**
* 审批通过
*/
public function approve(int $orderId, int $approverId, ?string $comment = null): ApprovalOrder
{
return $this->db->transaction(function () use ($orderId, $approverId, $comment) {
$order = ApprovalOrder::with(['flow.nodes'])->findOrFail($orderId);
// 检查是否为自己审批自己的申请 - 自动通过
if ($order->applicant_id === $approverId) {
error_log('🔄 检测到自审批,自动通过');
return $this->autoApproveSelfNode($order, $approverId);
}
if (!$order->canApprove()) {
throw new \Exception('当前状态无法审批');
}
$currentNodeIds = $order->current_nodes ?? [];
error_log("🔍 当前节点 IDs: " . json_encode($currentNodeIds));
$currentNode = ApprovalNode::whereIn('id', $currentNodeIds)->first();
if (!$currentNode) {
error_log("❌ 未找到节点,IDs: " . json_encode($currentNodeIds));
throw new \Exception('当前节点不存在');
}
// 检查当前用户是否已经在当前节点审批过
$existingRecord = ApprovalRecord::where('order_id', $order->id)
->where('node_id', $currentNode->id)
->where('approver_id', $approverId)
->whereIn('action', [ApprovalRecord::ACTION_APPROVE, ApprovalRecord::ACTION_REJECT])
->first();
if ($existingRecord) {
$actionText = $existingRecord->action === ApprovalRecord::ACTION_APPROVE ? '通过' : '拒绝';
throw new \Exception("您已经{$actionText}过此申请,无需重复操作");
}
// 记录审批操作
ApprovalRecord::create([
'order_id' => $order->id,
'node_id' => $currentNode->id,
'node_type' => $currentNode->type,
'approver_id' => $approverId,
'action' => ApprovalRecord::ACTION_APPROVE,
'comment' => $comment,
]);
// 判断节点是否完成
if ($this->isNodeCompleted($order->id, $currentNode->id)) {
// 对于或签和单人审批,更新其他审批人的消息卡片
if ($currentNode->type === ApprovalNode::TYPE_OR_APPROVE || $currentNode->type === 'approver') {
$approver = User::find($approverId);
$approverName = $approver ? $approver->name : '未知';
if ($this->notificationService) {
$this->notificationService->updateMessageCards(
$order->id,
$currentNode->id,
$approverId,
'approved',
$approverName
);
}
}
// 移动到下一节点
$nextNodes = $this->getNextNodes($order->flow, $currentNode->order);
if (empty($nextNodes)) {
// 没有下一节点,审批完成
$order->status = ApprovalOrder::STATUS_APPROVED;
$order->current_nodes = [];
// 通知申请人审批通过
$this->notifyApplicant($order, 'approve');
} else {
// 检查第一个下一节点
$firstNextNode = $nextNodes[0] ?? null;
if ($firstNextNode && $firstNextNode['type'] === 'notify') {
// 下一个是抄送节点,跳过并完成流程
$order->status = ApprovalOrder::STATUS_APPROVED;
$order->current_nodes = [];
// 通知申请人审批通过
$this->notifyApplicant($order, 'approve');
} else {
// 下一个是审批节点,移动到该节点
$order->current_nodes = collect($nextNodes)->pluck('id')->toArray();
// 通知下一节点的审批人
$this->notifyNextApprovers($order, $nextNodes);
}
}
$order->save();
}
return $order->fresh(['flow', 'applicant', 'records']);
});
}
/**
* 审批拒绝
*/
public function reject(int $orderId, int $approverId, ?string $comment = null): ApprovalOrder
{
return $this->db->transaction(function () use ($orderId, $approverId, $comment) {
$order = ApprovalOrder::findOrFail($orderId);
// 检查是否为自己审批自己的申请 - 自动取消
if ($order->applicant_id === $approverId) {
error_log('🔄 检测到自审批拒绝,自动取消申请');
return $this->cancel($orderId, $approverId);
}
if (!$order->canApprove()) {
throw new \Exception('当前状态无法审批');
}
$currentNodeIds = $order->current_nodes ?? [];
$currentNode = ApprovalNode::whereIn('id', $currentNodeIds)->first();
// 记录拒绝操作
ApprovalRecord::create([
'order_id' => $order->id,
'node_id' => $currentNode->id ?? 0,
'node_type' => $currentNode->type ?? 'unknown',
'approver_id' => $approverId,
'action' => ApprovalRecord::ACTION_REJECT,
'comment' => $comment,
]);
// 更新其他审批人的消息卡片(拒绝时所有人都需要更新)
$approver = User::find($approverId);
$approverName = $approver ? $approver->name : '未知';
if ($this->notificationService && $currentNode) {
$this->notificationService->updateMessageCards(
$order->id,
$currentNode->id,
$approverId,
'rejected',
$approverName
);
}
// 更新状态为已拒绝
$order->status = ApprovalOrder::STATUS_REJECTED;
$order->current_nodes = [];
$order->save();
// 通知申请人审批被拒绝
$this->notifyApplicant($order, 'reject', $comment);
return $order->fresh(['flow', 'applicant', 'records']);
});
}
/**
* 撤销申请
*/
public function cancel(int $orderId, int $userId): ApprovalOrder
{
$order = ApprovalOrder::findOrFail($orderId);
if ($order->applicant_id !== $userId) {
throw new \Exception('只有申请人可以撤销');
}
if (!$order->canEdit() && !$order->canApprove()) {
throw new \Exception('当前状态无法撤销');
}
// 记录撤销操作
ApprovalRecord::create([
'order_id' => $order->id,
'node_id' => 0,
'node_type' => 'cancel',
'approver_id' => $userId,
'action' => ApprovalRecord::ACTION_CANCEL,
'comment' => '申请人撤销',
]);
$order->status = ApprovalOrder::STATUS_CANCELLED;
$order->current_nodes = [];
$order->save();
return $order->fresh(['flow', 'applicant', 'records']);
}
/**
* 获取审批列表
*/
public function getList(int $userId, string $type = 'my', int $page = 1, int $pageSize = 20): array
{
$query = ApprovalOrder::with(['flow', 'applicant']);
switch ($type) {
case 'my':
$query->where('applicant_id', $userId);
error_log("🔍 Debug 'my' query: userId={$userId}");
break;
case 'pending':
// 获取用户待审批的订单
$pendingOrderIds = $this->getPendingOrderIds($userId);
error_log("🔍 Debug 'pending' query: userId={$userId}, pendingOrderIds=" . json_encode($pendingOrderIds));
$query->whereIn('id', $pendingOrderIds);
break;
case 'approved':
// 获取用户已审批的订单(只显示已完成的:approved、rejected、cancelled)
$approvedOrderIds = ApprovalRecord::where('approver_id', $userId)
->where('action', '!=', ApprovalRecord::ACTION_SUBMIT)
->whereIn('action', [ApprovalRecord::ACTION_APPROVE, ApprovalRecord::ACTION_REJECT])
->pluck('order_id')
->unique()
->toArray();
error_log("🔍 Debug 'approved' query: userId={$userId}, approvedOrderIds=" . json_encode($approvedOrderIds));
$query->whereIn('id', $approvedOrderIds)
->whereIn('status', [ApprovalOrder::STATUS_APPROVED, ApprovalOrder::STATUS_REJECTED, ApprovalOrder::STATUS_CANCELLED]);
break;
case 'cc':
// 获取抄送给我的订单
$allOrders = ApprovalOrder::where('status', '!=', ApprovalOrder::STATUS_DRAFT)->get();
$ccOrderIds = [];
foreach ($allOrders as $order) {
$formData = $order->form_data ?? [];
$ccUsers = $formData['cc_users'] ?? [];
// 兼容两种格式:[{id:29, name:'...'}] 或 [29] 或 [{id:29}]
if (!empty($ccUsers)) {
if (is_array($ccUsers[0] ?? null) && isset($ccUsers[0]['id'])) {
// 对象数组格式: [{id:29, name:'...'}]
$ccUserIds = array_column($ccUsers, 'id');
} else if (is_numeric($ccUsers[0] ?? null)) {
// ID数组格式: [29, 30]
$ccUserIds = $ccUsers;
} else {
$ccUserIds = [];
}
if (in_array($userId, $ccUserIds)) {
$ccOrderIds[] = $order->id;
}
}
}
error_log("🔍 Debug 'cc' query: userId={$userId}, ccOrderIds=" . json_encode($ccOrderIds));
$query->whereIn('id', $ccOrderIds);
break;
}
$total = $query->count();
error_log("🔍 Debug total count: type={$type}, total={$total}");
$list = $query->orderBy('created_at', 'desc')
->offset(($page - 1) * $pageSize)
->limit($pageSize)
->get();
error_log("🔍 Debug list count: type={$type}, page={$page}, count=" . $list->count());
// 转换为数组避免序列化问题
$listArray = $list->map(function ($item) {
return [
'id' => $item->id,
'title' => $item->title,
'business_type' => $item->business_type,
'status' => $item->status,
'created_at' => $item->created_at ? $item->created_at->toDateTimeString() : null,
'applicant' => $item->applicant ? [
'id' => $item->applicant->id,
'name' => $item->applicant->name,
] : null,
'flow' => $item->flow ? [
'id' => $item->flow->id,
'name' => $item->flow->name,
] : null,
];
})->toArray();
return [
'list' => $listArray,
'total' => $total,
'page' => $page,
'page_size' => $pageSize,
];
}
/**
* 获取第一个节点ID列表
*/
private function getFirstNodeIds(ApprovalFlow $flow): array
{
return $flow->nodes
->where('order', 0)
->pluck('id')
->toArray();
}
/**
* 判断节点是否完成
*/
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'] ?? [];
// 兼容两种格式:[{id:1, name:'...'}] 或 [1]
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;
}
}
/**
* 获取下一节点
*/
private function getNextNodes(ApprovalFlow $flow, int $currentOrder): array
{
return $flow->nodes
->where('order', '>', $currentOrder)
->sortBy('order')
->values()
->toArray();
}
/**
* 获取用户待审批的订单ID列表
*/
private function getPendingOrderIds(int $userId): array
{
// 获取用户参与的待审批订单
$pendingOrders = ApprovalOrder::where('status', ApprovalOrder::STATUS_PENDING)
->whereNotNull('current_nodes')
->get();
$result = [];
foreach ($pendingOrders as $order) {
// 从 form_data 获取审批人列表
$formData = $order->form_data ?? [];
$approvers = $formData['approvers'] ?? [];
// 兼容两种格式:[{id:1, name:'...'}] 或 [1]
if (!empty($approvers)) {
if (is_array($approvers[0] ?? null) && isset($approvers[0]['id'])) {
// 对象数组格式: [{id:1, name:'...'}]
$approverIds = array_column($approvers, 'id');
} else {
// ID数组格式: [1, 2, 3]
$approverIds = $approvers;
}
if (in_array($userId, $approverIds)) {
// 检查是否已审批
$hasApproved = ApprovalRecord::where('order_id', $order->id)
->where('approver_id', $userId)
->whereIn('action', [ApprovalRecord::ACTION_APPROVE, ApprovalRecord::ACTION_REJECT])
->exists();
if (!$hasApproved) {
$result[] = $order->id;
}
}
}
}
return $result;
}
/**
* 通知申请人
*/
private function notifyApplicant(ApprovalOrder $order, string $action, ?string $comment = null): void
{
try {
if (!$this->notificationService) {
$this->notificationService = new FeishuNotificationService();
}
$applicant = User::find($order->applicant_id);
if (!$applicant) {
error_log("❌ notifyApplicant: 申请人不存在, applicant_id={$order->applicant_id}");
return;
}
// 根据业务类型获取详情链接
$detailUrl = $this->getDetailUrl($order);
// 构建消息内容
$title = $action === 'approve' ? '您的申请已通过' : '您的申请已被拒绝';
$content = "**申请标题**: {$order->title}\n";
$content .= "**申请类型**: {$this->getBusinessTypeName($order->business_type)}\n";
if ($comment) {
$content .= "**审批意见**: {$comment}\n";
}
error_log("🔔 通知申请人: applicant_id={$applicant->id}, action={$action}");
$this->notificationService->setAppByUser($applicant->id)
->sendSystemNotification([
'user_ids' => [$applicant->id],
'title' => $title,
'content' => $content,
'url' => $detailUrl,
]);
} catch (\Exception $e) {
error_log('❌ notifyApplicant 失败: ' . $e->getMessage());
}
}
/**
* 通知下一节点的审批人
*/
private function notifyNextApprovers(ApprovalOrder $order, array $nextNodes): void
{
try {
if (!$this->notificationService) {
$this->notificationService = new FeishuNotificationService();
}
// 从 form_data 获取审批人列表
$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 (empty($approverIds)) {
error_log("⚠️ notifyNextApprovers: 没有找到审批人, order_id={$order->id}");
return;
}
// 获取详情链接
$detailUrl = $this->getDetailUrl($order);
// 业务类型名称
$businessTypeName = $this->getBusinessTypeName($order->business_type);
error_log("🔔 通知下一节点审批人: order_id={$order->id}, approverIds=" . json_encode($approverIds));
// 获取第一个下一节点的 ID(用于消息映射)
$nextNodeId = $nextNodes[0]['id'] ?? null;
// 为每个审批人发送通知
foreach ($approverIds as $approverId) {
try {
$this->notificationService->setAppByUser($approverId)
->sendApprovalNotification([
'user_id' => $approverId,
'applicant_id' => $order->applicant_id,
'title' => '新的审批待办',
'applicant' => $order->applicant->name,
'type' => $businessTypeName,
'content' => "**申请标题**: {$order->title}\n**申请人**: {$order->applicant->name}",
'url' => $detailUrl,
'order_id' => $order->id,
'node_id' => $nextNodeId, // 添加节点ID用于保存消息映射
]);
error_log("✅ 审批人通知成功: approver_id={$approverId}");
} catch (\Exception $e) {
error_log("❌ 发送审批通知失败: approver_id={$approverId}, error=" . $e->getMessage());
}
}
} catch (\Exception $e) {
error_log('❌ notifyNextApprovers 失败: ' . $e->getMessage());
}
}
/**
* 获取详情链接
*/
private function getDetailUrl(ApprovalOrder $order): string
{
$baseUrl = config('feishu_notification.base_url');
// 如果没有配置,使用当前请求构建
if (!$baseUrl) {
// 检查反向代理的协议头
$scheme = request()->header('x-forwarded-proto') ?? request()->header('x-scheme') ?? 'https';
$host = request()->host();
$baseUrl = $scheme . '://' . $host;
}
// 统一使用审批详情页,不再跳转到具体业务模块的详情页
return "{$baseUrl}/approval/detail/{$order->id}";
}
/**
* 获取业务类型名称
*/
private function getBusinessTypeName(string $businessType): string
{
$typeNames = [
'business_trip' => '出差申请',
'expense' => '费用报销',
'leave' => '请假申请',
'purchase' => '采购申请',
];
return $typeNames[$businessType] ?? $businessType;
}
/**
* 自动通过申请人自己的审批节点
* 当审批人是申请人自己时,自动通过当前节点
*/
private function autoApproveSelfNode(ApprovalOrder $order, int $approverId): ApprovalOrder
{
$currentNodeIds = $order->current_nodes ?? [];
error_log("🔄 自审批自动通过 - 当前节点: " . json_encode($currentNodeIds));
// 检查订单是否已经结束
if (empty($currentNodeIds)) {
error_log("❌ 自审批:订单已完成,current_nodes为空");
if ($order->status === ApprovalOrder::STATUS_APPROVED) {
throw new \Exception('该申请已通过,无需重复操作');
} else if ($order->status === ApprovalOrder::STATUS_REJECTED) {
throw new \Exception('该申请已被拒绝');
} else if ($order->status === ApprovalOrder::STATUS_CANCELLED) {
throw new \Exception('该申请已被撤销');
}
throw new \Exception('该申请已完成,无法重复操作');
}
$currentNode = ApprovalNode::whereIn('id', $currentNodeIds)->first();
if (!$currentNode) {
error_log("❌ 自审批:当前节点不存在,IDs: " . json_encode($currentNodeIds));
throw new \Exception('当前节点不存在');
}
// 记录自动审批操作
ApprovalRecord::create([
'order_id' => $order->id,
'node_id' => $currentNode->id,
'node_type' => $currentNode->type,
'approver_id' => $approverId,
'action' => ApprovalRecord::ACTION_APPROVE,
'comment' => '系统自动通过(申请人自审批)',
]);
// 检查节点是否完成(单人审批节点会完成,会签节点需要所有人审批)
if ($this->isNodeCompleted($order->id, $currentNode->id)) {
error_log("✅ 自审批节点完成,移动到下一节点");
// 移动到下一节点
$nextNodes = $this->getNextNodes($order->flow, $currentNode->order);
if (empty($nextNodes)) {
// 流程结束
$order->status = ApprovalOrder::STATUS_APPROVED;
$order->current_nodes = [];
error_log("✅ 自审批完成整个流程");
// 通知申请人审批通过
$this->notifyApplicant($order, 'approve');
} else {
// 下一个是通知节点或审批节点
$isNotify = collect($nextNodes)->every(fn($n) => $n->type === 'notify');
if ($isNotify) {
// 下一个是通知节点,流程结束
$order->status = ApprovalOrder::STATUS_APPROVED;
$order->current_nodes = [];
error_log("✅ 自审批后到达通知节点,流程完成");
// 通知申请人审批通过
$this->notifyApplicant($order, 'approve');
} else {
// 下一个是审批节点,移动到该节点
$order->current_nodes = collect($nextNodes)->pluck('id')->toArray();
error_log("➡️ 自审批后移动到下一节点: " . json_encode($order->current_nodes));
// 通知下一节点的审批人
$this->notifyNextApprovers($order, $nextNodes);
}
}
} else {
error_log("⏳ 自审批节点未完成(会签节点),等待其他审批人");
}
$order->save();
return $order->fresh(['flow', 'applicant', 'records']);
}
}