Files
x-server-mobile/backend/app/service/ApprovalService.php
T
root 1004aea725 feat: 审批功能优化和拒绝原因弹窗
后端改动:
- ApprovalService: 添加调试日志,简化详情页 URL 生成逻辑
- BusinessTripService: 优化出差审批详情展示

前端改动:
- approval/detail: 审批详情页优化,支持查看完整审批流程
- approval/reject: 新增拒绝原因弹窗组件
- business-trip/detail: 出差详情页增强,展示审批状态和操作

功能特性:
- 审批详情页显示完整审批流程和记录
- 拒绝操作支持填写拒绝原因
- 出差申请详情页优化展示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:57:56 +08:00

526 lines
19 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->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('当前节点不存在');
}
// 记录审批操作
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)) {
// 移动到下一节点
$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->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,
]);
// 更新状态为已拒绝
$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));
// 为每个审批人发送通知
foreach ($approverIds as $approverId) {
try {
$this->notificationService->setAppByUser($approverId)
->sendApprovalNotification([
'user_id' => $approverId,
'title' => '新的审批待办',
'applicant' => $order->applicant->name,
'type' => $businessTypeName,
'content' => "**申请标题**: {$order->title}\n**申请人**: {$order->applicant->name}",
'url' => $detailUrl,
'order_id' => $order->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 = 'https://xsm.dev.hnxinyouhe.com';
// 统一使用审批详情页,不再跳转到具体业务模块的详情页
return "{$baseUrl}/approval/detail/{$order->id}";
}
/**
* 获取业务类型名称
*/
private function getBusinessTypeName(string $businessType): string
{
$typeNames = [
'business_trip' => '出差申请',
'expense' => '费用报销',
'leave' => '请假申请',
'purchase' => '采购申请',
];
return $typeNames[$businessType] ?? $businessType;
}
}