09b2902d2b
从模型的 start_time 和 end_time 字段读取数据,而不是错误的 start_date 和 end_date。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
974 lines
40 KiB
PHP
974 lines
40 KiB
PHP
<?php
|
|
|
|
namespace app\service;
|
|
|
|
use app\model\BusinessTrip;
|
|
use app\model\BusinessTripItinerary;
|
|
use app\model\ApprovalOrder;
|
|
use app\model\ApprovalFlow;
|
|
use app\model\ApprovalNode;
|
|
use app\model\ApprovalRecord;
|
|
use app\model\User;
|
|
use support\Db;
|
|
use Illuminate\Database\Connection;
|
|
|
|
class BusinessTripService
|
|
{
|
|
private Connection $db;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->db = Db::connection();
|
|
}
|
|
|
|
/**
|
|
* 创建出差申请(草稿)
|
|
*/
|
|
public function createDraft(int $userId, array $data): BusinessTrip
|
|
{
|
|
return $this->db->transaction(function () use ($userId, $data) {
|
|
try {
|
|
// 计算最早和最晚时间
|
|
$itineraries = $data['itineraries'] ?? [];
|
|
if (empty($itineraries)) {
|
|
throw new \Exception('请至少添加一条行程');
|
|
}
|
|
|
|
$startDate = null;
|
|
$endDate = null;
|
|
$destinations = [];
|
|
|
|
foreach ($itineraries as $itinerary) {
|
|
if ($itinerary['start_time']) {
|
|
if (!$startDate || $itinerary['start_time'] < $startDate) {
|
|
$startDate = $itinerary['start_time'];
|
|
}
|
|
}
|
|
if ($itinerary['end_time']) {
|
|
if (!$endDate || $itinerary['end_time'] > $endDate) {
|
|
$endDate = $itinerary['end_time'];
|
|
}
|
|
}
|
|
if (!empty($itinerary['destination'])) {
|
|
$destinations[] = $itinerary['destination'];
|
|
}
|
|
}
|
|
|
|
// 创建出差申请
|
|
$businessTrip = BusinessTrip::create([
|
|
'user_id' => $userId,
|
|
'reason' => $data['reason'] ?? '',
|
|
'start_date' => $startDate ? date('Y-m-d', strtotime($startDate)) : null,
|
|
'end_date' => $endDate ? date('Y-m-d', strtotime($endDate)) : null,
|
|
'destination' => implode(', ', array_unique($destinations)),
|
|
'estimated_cost' => $data['estimated_cost'] ?? 0,
|
|
'status' => BusinessTrip::STATUS_DRAFT,
|
|
]);
|
|
|
|
// 创建行程明细
|
|
foreach ($itineraries as $itinerary) {
|
|
// 处理空字符串和空值
|
|
$startTime = !empty($itinerary['start_time']) ? $itinerary['start_time'] : null;
|
|
$endTime = !empty($itinerary['end_time']) ? $itinerary['end_time'] : null;
|
|
|
|
BusinessTripItinerary::create([
|
|
'business_trip_id' => $businessTrip->id,
|
|
'start_time' => $startTime,
|
|
'end_time' => $endTime,
|
|
'departure' => $itinerary['departure'] ?? '',
|
|
'destination' => $itinerary['destination'] ?? '',
|
|
'transport' => $itinerary['transport'] ?? '',
|
|
'trip_type' => $itinerary['trip_type'] ?? 'oneway',
|
|
'remark' => $itinerary['remark'] ?? '',
|
|
]);
|
|
}
|
|
|
|
return $businessTrip->load('itineraries');
|
|
} catch (\Exception $e) {
|
|
error_log('BusinessTripService::createDraft error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
|
throw $e;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 提交出差申请
|
|
*/
|
|
public function submit(int $userId, int $businessTripId, array $approvers, array $ccUsers = []): BusinessTrip
|
|
{
|
|
return $this->db->transaction(function () use ($userId, $businessTripId, $approvers, $ccUsers) {
|
|
$businessTrip = BusinessTrip::with('itineraries')->findOrFail($businessTripId);
|
|
|
|
if ($businessTrip->user_id !== $userId) {
|
|
throw new \Exception('无权操作此申请');
|
|
}
|
|
|
|
if (!$businessTrip->canSubmit()) {
|
|
throw new \Exception('当前状态无法提交');
|
|
}
|
|
|
|
// 验证审批人和抄送人不能重复
|
|
// 兼容两种格式:对象数组 [{id:1, name:'xxx'}] 或纯ID数组 [1, 2, 3]
|
|
if (!empty($approvers) && isset($approvers[0]['id'])) {
|
|
$approverIds = array_column($approvers, 'id');
|
|
} else {
|
|
$approverIds = $approvers;
|
|
}
|
|
|
|
if (!empty($ccUsers) && isset($ccUsers[0]['id'])) {
|
|
$ccUserIds = array_column($ccUsers, 'id');
|
|
} else {
|
|
$ccUserIds = $ccUsers;
|
|
}
|
|
|
|
// 检查审批人列表中是否有重复
|
|
if (count($approverIds) !== count(array_unique($approverIds))) {
|
|
throw new \Exception('审批人列表中有重复用户');
|
|
}
|
|
|
|
// 检查抄送人列表中是否有重复
|
|
if (count($ccUserIds) !== count(array_unique($ccUserIds))) {
|
|
throw new \Exception('抄送人列表中有重复用户');
|
|
}
|
|
|
|
// 检查审批人和抄送人是否有重叠
|
|
$overlap = array_intersect($approverIds, $ccUserIds);
|
|
if (!empty($overlap)) {
|
|
throw new \Exception('审批人和抄送人不能是同一个人');
|
|
}
|
|
|
|
// 获取审批流程
|
|
$flow = ApprovalFlow::where('business_type', 'business_trip')
|
|
->where('status', 1)
|
|
->first();
|
|
|
|
if (!$flow) {
|
|
// 创建默认流程
|
|
$flow = ApprovalFlow::create([
|
|
'business_type' => 'business_trip',
|
|
'name' => '出差审批',
|
|
'description' => '出差申请审批流程',
|
|
'status' => 1,
|
|
]);
|
|
}
|
|
|
|
// approval_node 是流程模板,不应在提交时修改
|
|
// 实际的审批人存储在 approval_order.form_data 中
|
|
// 获取或创建第一个审批节点(仅用于流程结构)
|
|
$node = ApprovalNode::where('flow_id', $flow->id)
|
|
->where('order', 0)
|
|
->first();
|
|
|
|
if (!$node) {
|
|
$node = ApprovalNode::create([
|
|
'flow_id' => $flow->id,
|
|
'type' => 'approver',
|
|
'name' => '审批',
|
|
'order' => 0,
|
|
'approvers' => [], // 模板节点不存储具体审批人
|
|
]);
|
|
}
|
|
|
|
// 创建审批单
|
|
$itinerariesData = $businessTrip->itineraries->map(function ($item) {
|
|
return [
|
|
'destination' => $item->destination,
|
|
'start_date' => $item->start_time,
|
|
'end_date' => $item->end_time,
|
|
'estimated_cost' => $item->estimated_cost ?? 0,
|
|
];
|
|
})->toArray();
|
|
|
|
error_log("🔍 itineraries 数据: " . json_encode($itinerariesData));
|
|
|
|
$approvalOrder = ApprovalOrder::create([
|
|
'business_type' => 'business_trip',
|
|
'business_id' => $businessTrip->id,
|
|
'title' => '出差申请 - ' . $businessTrip->user->name,
|
|
'applicant_id' => $userId,
|
|
'status' => ApprovalOrder::STATUS_PENDING,
|
|
'current_nodes' => [$node->id], // 使用创建的节点 ID
|
|
'flow_id' => $flow->id,
|
|
'form_data' => [
|
|
'reason' => $businessTrip->reason,
|
|
'start_date' => $businessTrip->start_date,
|
|
'end_date' => $businessTrip->end_date,
|
|
'destination' => $businessTrip->destination,
|
|
'estimated_cost' => $businessTrip->estimated_cost,
|
|
'itineraries' => $itinerariesData,
|
|
'approvers' => $approvers,
|
|
'cc_users' => $ccUsers,
|
|
],
|
|
]);
|
|
|
|
// 更新出差申请状态
|
|
$businessTrip->status = BusinessTrip::STATUS_PENDING;
|
|
$businessTrip->approval_order_id = $approvalOrder->id;
|
|
$businessTrip->save();
|
|
|
|
// 记录提交操作
|
|
ApprovalRecord::create([
|
|
'order_id' => $approvalOrder->id,
|
|
'node_id' => $node->id,
|
|
'node_type' => $node->type,
|
|
'approver_id' => $userId,
|
|
'action' => 'submit',
|
|
'comment' => null,
|
|
]);
|
|
|
|
// 如果发起人在审批人列表中,自动标记为已审批
|
|
if (in_array($userId, $approverIds)) {
|
|
ApprovalRecord::create([
|
|
'order_id' => $approvalOrder->id,
|
|
'node_id' => $node->id,
|
|
'node_type' => $node->type,
|
|
'approver_id' => $userId,
|
|
'action' => 'approve',
|
|
'comment' => '发起人自动审批通过',
|
|
]);
|
|
|
|
// 判断节点是否完成
|
|
$nodeCompleted = false;
|
|
|
|
if ($node->type === 'approver') {
|
|
// 单人审批:发起人即完成
|
|
$nodeCompleted = true;
|
|
} elseif ($node->type === 'or_approve') {
|
|
// 或签:任一人通过即可
|
|
$nodeCompleted = true;
|
|
} elseif ($node->type === 'and_approve') {
|
|
// 会签:需要所有人通过
|
|
$approvedCount = ApprovalRecord::where('order_id', $approvalOrder->id)
|
|
->where('node_id', $node->id)
|
|
->where('action', 'approve')
|
|
->count();
|
|
|
|
if ($approvedCount >= count($approverIds)) {
|
|
$nodeCompleted = true;
|
|
}
|
|
}
|
|
|
|
// 如果节点完成,移动到下一节点
|
|
if ($nodeCompleted) {
|
|
// 获取当前节点的 order 值
|
|
$currentOrder = $node->order ?? 0;
|
|
|
|
// 查找 order 比当前节点大的后续节点
|
|
$nextNodes = ApprovalNode::where('flow_id', $flow->id)
|
|
->where('order', '>', $currentOrder)
|
|
->orderBy('order')
|
|
->get();
|
|
|
|
if ($nextNodes->isEmpty()) {
|
|
// 没有后续节点,审批完成
|
|
$approvalOrder->status = ApprovalOrder::STATUS_APPROVED;
|
|
$approvalOrder->current_nodes = [];
|
|
$businessTrip->status = BusinessTrip::STATUS_APPROVED;
|
|
} else {
|
|
// 检查第一个后续节点
|
|
$firstNextNode = $nextNodes->first();
|
|
if ($firstNextNode && $firstNextNode->type === 'notify') {
|
|
// 下一个是抄送节点,跳过并完成流程
|
|
$approvalOrder->status = ApprovalOrder::STATUS_APPROVED;
|
|
$approvalOrder->current_nodes = [];
|
|
$businessTrip->status = BusinessTrip::STATUS_APPROVED;
|
|
} else {
|
|
// 移动到下一节点
|
|
$approvalOrder->current_nodes = $nextNodes->pluck('id')->toArray();
|
|
}
|
|
}
|
|
|
|
$approvalOrder->save();
|
|
$businessTrip->save();
|
|
}
|
|
}
|
|
|
|
// 发送飞书通知给审批人
|
|
error_log("🔔 准备发送审批通知,调用 sendApprovalNotification 方法");
|
|
error_log("📋 传递的参数 - approverIds: " . json_encode($approverIds) . ", ccUserIds: " . json_encode($ccUserIds));
|
|
$this->sendApprovalNotification($businessTrip, $approverIds, $ccUserIds, $node);
|
|
|
|
return $businessTrip->fresh(['itineraries', 'approvalOrder']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 发送审批通知(飞书消息)
|
|
*/
|
|
private function sendApprovalNotification(BusinessTrip $businessTrip, array $approverIds, array $ccUserIds, ApprovalNode $node): void
|
|
{
|
|
try {
|
|
error_log("🔔 准备发送审批通知: trip_id={$businessTrip->id}, approvers=" . json_encode($approverIds) . ", cc=" . json_encode($ccUserIds));
|
|
|
|
$notificationService = new \app\service\FeishuNotificationService();
|
|
|
|
// 获取 approval_order_id
|
|
$approvalOrder = \app\model\ApprovalOrder::where('business_type', 'business_trip')
|
|
->where('business_id', $businessTrip->id)
|
|
->first();
|
|
|
|
if (!$approvalOrder) {
|
|
error_log("⚠️ 未找到审批单,无法发送通知");
|
|
return;
|
|
}
|
|
|
|
// 构建审批链接(统一使用审批详情页)
|
|
$approvalUrl = "https://uniservice-mobile.hnxinyouhe.com/approval/detail/{$approvalOrder->id}";
|
|
$detailUrl = $approvalUrl; // 用于抄送人和其他通知
|
|
|
|
// 构建内容
|
|
$content = "**出差原因**: {$businessTrip->reason}\n";
|
|
$content .= "**目的地**: {$businessTrip->destination}\n";
|
|
$content .= "**时间**: {$businessTrip->start_date} 至 {$businessTrip->end_date}";
|
|
|
|
// 通知审批人(带审批按钮)
|
|
foreach ($approverIds as $approverId) {
|
|
try {
|
|
error_log("📧 通知审批人: approver_id={$approverId}");
|
|
$notificationService->setAppByUser($approverId)
|
|
->sendApprovalNotification([
|
|
'user_id' => $approverId,
|
|
'applicant_id' => $approvalOrder->applicant_id, // 添加申请人ID用于自审批检测
|
|
'title' => '新的出差审批申请',
|
|
'applicant' => $businessTrip->user->name,
|
|
'type' => '出差申请',
|
|
'content' => $content,
|
|
'url' => $approvalUrl,
|
|
'order_id' => $approvalOrder->id,
|
|
'node_id' => $node->id, // 添加节点ID用于保存消息映射
|
|
]);
|
|
error_log("✅ 审批人通知成功: approver_id={$approverId}");
|
|
} catch (\Exception $e) {
|
|
error_log("❌ 发送审批通知失败: approver_id={$approverId}, error=" . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// 通知抄送人(不带审批按钮)
|
|
foreach ($ccUserIds as $ccUserId) {
|
|
try {
|
|
error_log("📧 通知抄送人: cc_user_id={$ccUserId}");
|
|
$notificationService->setAppByUser($ccUserId)
|
|
->sendSystemNotification([
|
|
'user_ids' => [$ccUserId],
|
|
'title' => '出差申请抄送',
|
|
'content' => "**申请人**: {$businessTrip->user->name}\n" . $content,
|
|
'url' => $detailUrl,
|
|
]);
|
|
error_log("✅ 抄送人通知成功: cc_user_id={$ccUserId}");
|
|
} catch (\Exception $e) {
|
|
error_log("❌ 发送抄送通知失败: cc_user_id={$ccUserId}, error=" . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
error_log("✅ 审批通知发送完成");
|
|
} catch (\Exception $e) {
|
|
error_log("❌ 发送飞书通知异常: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 发送审批结果通知(飞书消息)
|
|
*/
|
|
private function sendApprovalResultNotification(BusinessTrip $businessTrip, string $action, string $comment = ''): void
|
|
{
|
|
try {
|
|
$notificationService = new \app\service\FeishuNotificationService();
|
|
|
|
// 获取关联的审批订单
|
|
$approvalOrder = \app\model\ApprovalOrder::where('business_id', $businessTrip->id)
|
|
->where('business_type', 'business_trip')
|
|
->first();
|
|
|
|
// 统一使用审批详情页
|
|
$detailUrl = $approvalOrder
|
|
? "https://uniservice-mobile.hnxinyouhe.com/approval/detail/{$approvalOrder->id}"
|
|
: "https://uniservice-mobile.hnxinyouhe.com/approval/list";
|
|
|
|
if ($action === 'approve') {
|
|
$title = '出差申请已通过';
|
|
$content = "您的出差申请已通过审批。\n";
|
|
$content .= "**申请原因**: {$businessTrip->reason}\n";
|
|
if ($comment) {
|
|
$content .= "**审批意见**: {$comment}";
|
|
}
|
|
} else {
|
|
$title = '出差申请已拒绝';
|
|
$content = "您的出差申请被拒绝。\n";
|
|
$content .= "**申请原因**: {$businessTrip->reason}\n";
|
|
if ($comment) {
|
|
$content .= "**拒绝原因**: {$comment}";
|
|
}
|
|
}
|
|
|
|
$notificationService->setAppByUser($businessTrip->user_id)
|
|
->sendSystemNotification([
|
|
'user_ids' => [$businessTrip->user_id],
|
|
'title' => $title,
|
|
'content' => $content,
|
|
'url' => $detailUrl,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
error_log("发送审批结果通知失败: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取出差申请列表
|
|
*/
|
|
public function getList(int $userId, string $type = 'my', int $page = 1, int $pageSize = 20): array
|
|
{
|
|
// 先不加 approvalOrder 关系,避免 PHP 8.5 兼容性问题
|
|
$query = BusinessTrip::with(['user', 'itineraries']);
|
|
|
|
switch ($type) {
|
|
case 'my':
|
|
// 我发起的:当前用户创建的申请
|
|
$query->where('user_id', $userId);
|
|
break;
|
|
case 'pending':
|
|
// 待我审批:需要查询当前用户是审批人且还未审批的申请
|
|
$allPendingOrders = ApprovalOrder::where('status', ApprovalOrder::STATUS_PENDING)
|
|
->where('business_type', 'business_trip')
|
|
->get();
|
|
error_log("🔍 Pending query result: count=" . count($allPendingOrders));
|
|
|
|
$pendingOrderIds = $allPendingOrders->pluck('id')->toArray();
|
|
|
|
if (empty($pendingOrderIds)) {
|
|
$query->whereRaw('1 = 0'); // 返回空结果
|
|
} else {
|
|
// 获取这些审批单的当前节点
|
|
$pendingOrders = ApprovalOrder::whereIn('id', $pendingOrderIds)->get();
|
|
$validOrderIds = [];
|
|
|
|
foreach ($pendingOrders as $order) {
|
|
error_log("🔍 Checking order {$order->id}: form_data=" . json_encode($order->form_data));
|
|
// 从 form_data 获取实际的审批人列表
|
|
$formData = $order->form_data ?? [];
|
|
$approvers = $formData['approvers'] ?? [];
|
|
|
|
// 兼容两种格式:[1,2,3] 或 [{id:1},{id:2},{id:3}]
|
|
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;
|
|
}
|
|
} else {
|
|
$approverIds = [];
|
|
}
|
|
|
|
error_log("🔍 Order {$order->id}: approverIds=" . json_encode($approverIds) . ", userId={$userId}");
|
|
// 检查用户是否在审批人列表中
|
|
$isApprover = in_array($userId, $approverIds);
|
|
error_log("🔍 Order {$order->id}: isApprover=" . ($isApprover ? 'true' : 'false'));
|
|
|
|
// 如果用户是当前节点审批人,且未审批过,则加入列表
|
|
if ($isApprover) {
|
|
$hasApproved = ApprovalRecord::where('order_id', $order->id)
|
|
->where('approver_id', $userId)
|
|
->whereIn('action', ['approve', 'reject'])
|
|
->exists();
|
|
|
|
if (!$hasApproved) {
|
|
$validOrderIds[] = $order->id;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($validOrderIds)) {
|
|
$query->whereRaw('1 = 0');
|
|
} else {
|
|
$query->whereIn('approval_order_id', $validOrderIds);
|
|
}
|
|
}
|
|
break;
|
|
case 'approved':
|
|
// 我已审批:当前用户已经审批过的申请
|
|
$approvedOrderIds = ApprovalRecord::where('approver_id', $userId)
|
|
->whereIn('action', ['approve', 'reject'])
|
|
->pluck('order_id')
|
|
->toArray();
|
|
|
|
error_log("🔍 'approved' query: userId={$userId}, approvedOrderIds=" . json_encode($approvedOrderIds));
|
|
|
|
if (empty($approvedOrderIds)) {
|
|
$query->whereRaw('1 = 0'); // 返回空结果
|
|
} else {
|
|
// 先检查是否有匹配的 business_trip(不加载关系)
|
|
$checkCount = BusinessTrip::whereIn('approval_order_id', $approvedOrderIds)->count();
|
|
error_log("🔍 Matching business_trips count (without relations): {$checkCount}");
|
|
|
|
// 获取匹配的 business_trip IDs
|
|
$matchingTrips = BusinessTrip::whereIn('approval_order_id', $approvedOrderIds)
|
|
->pluck('id')
|
|
->toArray();
|
|
error_log("🔍 Matching business_trip IDs: " . json_encode($matchingTrips));
|
|
|
|
$query->whereIn('approval_order_id', $approvedOrderIds);
|
|
}
|
|
break;
|
|
}
|
|
|
|
$total = $query->count();
|
|
$list = $query->orderBy('created_at', 'desc')
|
|
->offset(($page - 1) * $pageSize)
|
|
->limit($pageSize)
|
|
->get();
|
|
|
|
return [
|
|
'list' => $list,
|
|
'total' => $total,
|
|
'page' => $page,
|
|
'page_size' => $pageSize,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取出差申请详情
|
|
*/
|
|
public function getDetail(int $id): array
|
|
{
|
|
$businessTrip = BusinessTrip::with(['user', 'itineraries'])
|
|
->findOrFail($id);
|
|
|
|
$result = $businessTrip->toArray();
|
|
|
|
// 获取审批单信息
|
|
if ($businessTrip->approval_order_id) {
|
|
$approvalOrder = ApprovalOrder::with(['records.approver'])
|
|
->find($businessTrip->approval_order_id);
|
|
|
|
if ($approvalOrder) {
|
|
$result['approval_order'] = $approvalOrder->toArray();
|
|
|
|
// 获取审批人和抄送人信息
|
|
$formData = $approvalOrder->form_data ?? [];
|
|
$result['approvers'] = $formData['approvers'] ?? [];
|
|
$result['cc_users'] = $formData['cc_users'] ?? [];
|
|
|
|
// 获取审批记录
|
|
$result['approval_records'] = $approvalOrder->records->toArray();
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 更新出差申请
|
|
*/
|
|
public function update(int $userId, int $id, array $data): BusinessTrip
|
|
{
|
|
$businessTrip = BusinessTrip::findOrFail($id);
|
|
|
|
if ($businessTrip->user_id !== $userId) {
|
|
throw new \Exception('无权操作此申请');
|
|
}
|
|
|
|
if (!$businessTrip->canEdit()) {
|
|
throw new \Exception('当前状态无法编辑');
|
|
}
|
|
|
|
return $this->db->transaction(function () use ($businessTrip, $data) {
|
|
// 更新基本信息
|
|
if (isset($data['reason'])) {
|
|
$businessTrip->reason = $data['reason'];
|
|
}
|
|
|
|
// 更新行程
|
|
if (isset($data['itineraries'])) {
|
|
// 删除旧行程
|
|
BusinessTripItinerary::where('business_trip_id', $businessTrip->id)->delete();
|
|
|
|
// 创建新行程
|
|
$startDate = null;
|
|
$endDate = null;
|
|
$destinations = [];
|
|
|
|
foreach ($data['itineraries'] as $itinerary) {
|
|
// 处理空字符串和空值
|
|
$startTime = !empty($itinerary['start_time']) ? $itinerary['start_time'] : null;
|
|
$endTime = !empty($itinerary['end_time']) ? $itinerary['end_time'] : null;
|
|
|
|
BusinessTripItinerary::create([
|
|
'business_trip_id' => $businessTrip->id,
|
|
'start_time' => $startTime,
|
|
'end_time' => $endTime,
|
|
'departure' => $itinerary['departure'] ?? '',
|
|
'destination' => $itinerary['destination'] ?? '',
|
|
'transport' => $itinerary['transport'] ?? '',
|
|
'trip_type' => $itinerary['trip_type'] ?? 'oneway',
|
|
'remark' => $itinerary['remark'] ?? '',
|
|
]);
|
|
|
|
if ($itinerary['start_time']) {
|
|
if (!$startDate || $itinerary['start_time'] < $startDate) {
|
|
$startDate = $itinerary['start_time'];
|
|
}
|
|
}
|
|
if ($itinerary['end_time']) {
|
|
if (!$endDate || $itinerary['end_time'] > $endDate) {
|
|
$endDate = $itinerary['end_time'];
|
|
}
|
|
}
|
|
if (!empty($itinerary['destination'])) {
|
|
$destinations[] = $itinerary['destination'];
|
|
}
|
|
}
|
|
|
|
// 更新日期和目的地
|
|
$businessTrip->start_date = $startDate ? date('Y-m-d', strtotime($startDate)) : null;
|
|
$businessTrip->end_date = $endDate ? date('Y-m-d', strtotime($endDate)) : null;
|
|
$businessTrip->destination = implode(', ', array_unique($destinations));
|
|
}
|
|
|
|
$businessTrip->save();
|
|
|
|
return $businessTrip->fresh('itineraries');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 取消出差申请
|
|
*/
|
|
public function cancel(int $userId, int $id): BusinessTrip
|
|
{
|
|
$businessTrip = BusinessTrip::findOrFail($id);
|
|
|
|
if ($businessTrip->user_id !== $userId) {
|
|
throw new \Exception('无权操作此申请');
|
|
}
|
|
|
|
if (!$businessTrip->canCancel()) {
|
|
throw new \Exception('当前状态无法取消');
|
|
}
|
|
|
|
$businessTrip->status = BusinessTrip::STATUS_CANCELLED;
|
|
$businessTrip->save();
|
|
|
|
// 同时取消审批单
|
|
if ($businessTrip->approval_order_id) {
|
|
$approvalOrder = ApprovalOrder::find($businessTrip->approval_order_id);
|
|
if ($approvalOrder) {
|
|
$approvalOrder->status = ApprovalOrder::STATUS_CANCELLED;
|
|
$approvalOrder->current_nodes = [];
|
|
$approvalOrder->save();
|
|
|
|
// 发送取消通知给审批人和抄送人
|
|
$this->sendCancelNotification($businessTrip, $approvalOrder);
|
|
}
|
|
}
|
|
|
|
return $businessTrip->fresh();
|
|
}
|
|
|
|
/**
|
|
* 发送取消通知(飞书消息)
|
|
*/
|
|
private function sendCancelNotification(BusinessTrip $businessTrip, ApprovalOrder $approvalOrder): void
|
|
{
|
|
try {
|
|
$notificationService = new \app\service\FeishuNotificationService();
|
|
|
|
// 统一使用审批详情页
|
|
$detailUrl = "https://uniservice-mobile.hnxinyouhe.com/approval/detail/{$approvalOrder->id}";
|
|
|
|
// 获取审批人和抄送人
|
|
$formData = $approvalOrder->form_data ?? [];
|
|
$approvers = $formData['approvers'] ?? [];
|
|
$ccUsers = $formData['cc_users'] ?? [];
|
|
|
|
$content = "**申请人**: {$businessTrip->user->name}\n";
|
|
$content .= "**出差原因**: {$businessTrip->reason}\n";
|
|
$content .= "**目的地**: {$businessTrip->destination}";
|
|
|
|
// 合并所有需要通知的用户ID
|
|
$allUserIds = array_unique(array_merge(
|
|
array_column($approvers, 'id'),
|
|
array_column($ccUsers, 'id')
|
|
));
|
|
|
|
foreach ($allUserIds as $notifyUserId) {
|
|
try {
|
|
$notificationService->setAppByUser($notifyUserId)
|
|
->sendSystemNotification([
|
|
'user_ids' => [$notifyUserId],
|
|
'title' => '出差申请已取消',
|
|
'content' => $content,
|
|
'url' => $detailUrl,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
error_log("发送取消通知失败: user_id={$notifyUserId}, error=" . $e->getMessage());
|
|
}
|
|
}
|
|
} catch (\Exception $e) {
|
|
error_log("发送取消通知异常: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 删除草稿
|
|
*/
|
|
public function deleteDraft(int $userId, int $id): bool
|
|
{
|
|
$businessTrip = BusinessTrip::findOrFail($id);
|
|
|
|
if ($businessTrip->user_id !== $userId) {
|
|
throw new \Exception('无权操作此申请');
|
|
}
|
|
|
|
if ($businessTrip->status !== BusinessTrip::STATUS_DRAFT) {
|
|
throw new \Exception('只能删除草稿');
|
|
}
|
|
|
|
return $this->db->transaction(function () use ($businessTrip) {
|
|
// 删除行程明细
|
|
BusinessTripItinerary::where('business_trip_id', $businessTrip->id)->delete();
|
|
|
|
// 删除申请
|
|
return $businessTrip->delete();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 审批操作
|
|
*/
|
|
public function approve(int $userId, int $businessTripId, string $action, string $comment = ''): BusinessTrip
|
|
{
|
|
error_log("📝 approve() 开始: userId={$userId}, businessTripId={$businessTripId}, action={$action}");
|
|
|
|
try {
|
|
$result = $this->db->transaction(function () use ($userId, $businessTripId, $action, $comment) {
|
|
error_log("🔄 Transaction 开始");
|
|
$businessTrip = BusinessTrip::with(['itineraries', 'approvalOrder'])->findOrFail($businessTripId);
|
|
|
|
if (!$businessTrip->approval_order_id) {
|
|
throw new \Exception('未找到审批单');
|
|
}
|
|
|
|
$approvalOrder = $businessTrip->approvalOrder;
|
|
|
|
if ($approvalOrder->status !== ApprovalOrder::STATUS_PENDING) {
|
|
throw new \Exception('当前状态无法审批');
|
|
}
|
|
|
|
// 检查用户是否是当前审批人
|
|
// 从 form_data 获取实际的审批人列表
|
|
$formData = $approvalOrder->form_data ?? [];
|
|
$approvers = $formData['approvers'] ?? [];
|
|
|
|
// 兼容两种格式:[1,2,3] 或 [{id:1},{id:2},{id:3}]
|
|
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;
|
|
}
|
|
} else {
|
|
$approverIds = [];
|
|
}
|
|
|
|
if (!in_array($userId, $approverIds)) {
|
|
throw new \Exception('您不是当前审批人');
|
|
}
|
|
|
|
// 检查是否已经审批过
|
|
$existingRecord = ApprovalRecord::where('order_id', $approvalOrder->id)
|
|
->where('approver_id', $userId)
|
|
->whereIn('action', ['approve', 'reject'])
|
|
->first();
|
|
|
|
if ($existingRecord) {
|
|
throw new \Exception('您已经审批过了');
|
|
}
|
|
|
|
// 获取当前节点ID
|
|
$currentNodes = $approvalOrder->current_nodes ?? [];
|
|
$currentNodeId = $currentNodes[0] ?? null;
|
|
|
|
// 记录审批操作
|
|
ApprovalRecord::create([
|
|
'order_id' => $approvalOrder->id,
|
|
'node_id' => $currentNodeId,
|
|
'node_type' => 'approver',
|
|
'approver_id' => $userId,
|
|
'action' => $action,
|
|
'comment' => $comment,
|
|
]);
|
|
|
|
// 更新审批单状态
|
|
if ($action === 'reject') {
|
|
// 拒绝:直接结束流程
|
|
error_log("❌ 审批拒绝,结束流程");
|
|
$approvalOrder->status = ApprovalOrder::STATUS_REJECTED;
|
|
$approvalOrder->current_nodes = [];
|
|
$businessTrip->status = BusinessTrip::STATUS_REJECTED;
|
|
} else {
|
|
error_log("✅ 审批同意,检查节点完成状态");
|
|
// 同意:检查当前节点是否完成,并处理流程流转
|
|
$nodeCompleted = false;
|
|
|
|
if ($currentNodeId) {
|
|
$node = ApprovalNode::find($currentNodeId);
|
|
error_log("🔍 当前节点: id={$currentNodeId}, type=" . ($node ? $node->type : 'null'));
|
|
|
|
if ($node) {
|
|
// 根据节点类型判断是否完成
|
|
if ($node->type === 'approver') {
|
|
// 单人审批:一人通过即完成
|
|
error_log("✅ 单人审批节点,一人通过即完成");
|
|
$nodeCompleted = true;
|
|
} elseif ($node->type === 'or_approve') {
|
|
// 或签:一人通过即完成
|
|
error_log("✅ 或签节点,一人通过即完成");
|
|
$nodeCompleted = true;
|
|
} elseif ($node->type === 'and_approve') {
|
|
// 会签:需要所有人都通过
|
|
$approvedCount = ApprovalRecord::where('order_id', $approvalOrder->id)
|
|
->where('node_id', $currentNodeId)
|
|
->where('action', 'approve')
|
|
->count();
|
|
error_log("🔍 会签节点: approvedCount={$approvedCount}, totalApprovers=" . count($approverIds));
|
|
if ($approvedCount >= count($approverIds)) {
|
|
$nodeCompleted = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
error_log("🔍 nodeCompleted: " . ($nodeCompleted ? 'true' : 'false'));
|
|
|
|
if ($nodeCompleted) {
|
|
// 当前节点完成,查找下一个节点
|
|
error_log("✅ 节点 {$currentNodeId} 已完成,查找下一节点");
|
|
|
|
// 获取当前节点的 order 值
|
|
$currentOrder = $node->order ?? 0;
|
|
error_log("📍 当前节点 order: {$currentOrder}");
|
|
|
|
// 查找 order 比当前节点大的下一个节点
|
|
$nextNode = ApprovalNode::where('flow_id', $approvalOrder->flow_id)
|
|
->where('order', '>', $currentOrder)
|
|
->orderBy('order')
|
|
->first();
|
|
|
|
error_log("🔍 下一节点: " . ($nextNode ? "id={$nextNode->id}, type={$nextNode->type}, name={$nextNode->name}, order={$nextNode->order}" : "null"));
|
|
|
|
if ($nextNode) {
|
|
// 有下一个节点
|
|
if ($nextNode->type === 'notify') {
|
|
// 下一个是抄送节点,跳过并完成流程
|
|
error_log("⏭️ 跳过抄送节点,完成流程");
|
|
$approvalOrder->status = ApprovalOrder::STATUS_APPROVED;
|
|
$approvalOrder->current_nodes = [];
|
|
$businessTrip->status = BusinessTrip::STATUS_APPROVED;
|
|
} else {
|
|
// 下一个是审批节点,移动到该节点
|
|
error_log("➡️ 移动到下一节点: {$nextNode->id}");
|
|
$approvalOrder->current_nodes = [$nextNode->id];
|
|
}
|
|
} else {
|
|
// 没有下一个节点,流程完成
|
|
error_log("✅ 没有下一节点,流程完成");
|
|
$approvalOrder->status = ApprovalOrder::STATUS_APPROVED;
|
|
$approvalOrder->current_nodes = [];
|
|
$businessTrip->status = BusinessTrip::STATUS_APPROVED;
|
|
}
|
|
}
|
|
}
|
|
|
|
error_log("💾 准备保存: order_status={$approvalOrder->status}, trip_status={$businessTrip->status}");
|
|
|
|
$approvalOrder->save();
|
|
$businessTrip->save();
|
|
|
|
error_log("✅ 保存完成: order_id={$approvalOrder->id}, trip_id={$businessTrip->id}");
|
|
|
|
// 发送审批结果通知
|
|
$this->sendApprovalResultNotification($businessTrip, $action, $comment);
|
|
|
|
error_log("✅ Transaction 成功,返回结果");
|
|
|
|
return $businessTrip->fresh(['itineraries', 'approvalOrder']);
|
|
});
|
|
|
|
error_log("📝 approve() 完成: result_id=" . $result->id);
|
|
return $result;
|
|
} catch (\Exception $e) {
|
|
error_log("❌ approve() 异常: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取默认审批人和抄送人
|
|
* 从审批流程配置表中获取
|
|
*/
|
|
public function getDefaultApprovers(int $userId): array
|
|
{
|
|
// 获取出差审批流程
|
|
$flow = ApprovalFlow::where('business_type', 'business_trip')
|
|
->where('status', 1)
|
|
->first();
|
|
|
|
if (!$flow) {
|
|
return [
|
|
'approvers' => [],
|
|
'cc_users' => [],
|
|
];
|
|
}
|
|
|
|
// 获取流程节点
|
|
$nodes = ApprovalNode::where('flow_id', $flow->id)
|
|
->orderBy('order')
|
|
->get();
|
|
|
|
$approvers = [];
|
|
$ccUsers = [];
|
|
|
|
foreach ($nodes as $node) {
|
|
$nodeApprovers = $node->approvers ?? [];
|
|
|
|
if (empty($nodeApprovers)) {
|
|
continue;
|
|
}
|
|
|
|
// 根据节点类型获取用户信息
|
|
$users = User::whereIn('id', $nodeApprovers)
|
|
->where('status', 1)
|
|
->get();
|
|
|
|
$userList = [];
|
|
foreach ($nodeApprovers as $userId) {
|
|
$user = $users->firstWhere('id', $userId);
|
|
if ($user) {
|
|
$userList[] = [
|
|
'id' => $user->id,
|
|
'name' => $user->name,
|
|
'avatar' => $user->avatar,
|
|
];
|
|
}
|
|
}
|
|
|
|
// 审批节点(approver, and_approve, or_approve)作为审批人
|
|
if ($node->isApprovalNode()) {
|
|
$approvers = array_merge($approvers, $userList);
|
|
}
|
|
// 通知节点(notify)作为抄送人
|
|
elseif ($node->type === ApprovalNode::TYPE_NOTIFY) {
|
|
$ccUsers = array_merge($ccUsers, $userList);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'approvers' => $approvers,
|
|
'cc_users' => $ccUsers,
|
|
];
|
|
}
|
|
}
|