04953b22dc
- 新增通用审批列表页面(我发起的/待我审批/我已审批) - 新增通用审批详情页面(支持审批通过/拒绝操作) - 修复审批列表过滤逻辑("我已审批"只显示已完成订单) - 修复登录页面未设置 Zustand store 的问题 - 修复 User 类型定义(id 改为 number 类型) - 修复草稿恢复时的日期转换问题 - 创建测试数据脚本(test_data.php) - 安装 webman 官方依赖包(database, redis, pagination 等) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
529 lines
19 KiB
PHP
529 lines
19 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) {
|
|
BusinessTripItinerary::create([
|
|
'business_trip_id' => $businessTrip->id,
|
|
'start_time' => $itinerary['start_time'],
|
|
'end_time' => $itinerary['end_time'] ?? null,
|
|
'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('当前状态无法提交');
|
|
}
|
|
|
|
// 获取审批流程(简化版:直接使用第一个审批人作为单人审批)
|
|
$flow = ApprovalFlow::where('business_type', 'business_trip')
|
|
->where('status', 1)
|
|
->first();
|
|
|
|
if (!$flow) {
|
|
// 创建默认流程
|
|
$flow = ApprovalFlow::create([
|
|
'business_type' => 'business_trip',
|
|
'name' => '出差审批',
|
|
'description' => '出差申请审批流程',
|
|
'status' => 1,
|
|
]);
|
|
}
|
|
|
|
// 创建审批单
|
|
$approvalOrder = ApprovalOrder::create([
|
|
'business_type' => 'business_trip',
|
|
'business_id' => $businessTrip->id,
|
|
'title' => '出差申请 - ' . $businessTrip->user->name,
|
|
'applicant_id' => $userId,
|
|
'status' => ApprovalOrder::STATUS_PENDING,
|
|
'current_nodes' => [1], // 简化:使用节点1
|
|
'flow_id' => $flow->id,
|
|
'form_data' => [
|
|
'reason' => $businessTrip->reason,
|
|
'start_date' => $businessTrip->start_date,
|
|
'end_date' => $businessTrip->end_date,
|
|
'destination' => $businessTrip->destination,
|
|
'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' => 1,
|
|
'node_type' => 'single_approve',
|
|
'approver_id' => $userId,
|
|
'action' => 'submit',
|
|
'comment' => null,
|
|
]);
|
|
|
|
return $businessTrip->fresh(['itineraries', 'approvalOrder']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 获取出差申请列表
|
|
*/
|
|
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':
|
|
// 待我审批:需要查询当前用户是审批人且还未审批的申请
|
|
$pendingOrderIds = ApprovalOrder::where('status', ApprovalOrder::STATUS_PENDING)
|
|
->where('business_type', 'business_trip')
|
|
->pluck('id')
|
|
->toArray();
|
|
|
|
if (empty($pendingOrderIds)) {
|
|
$query->whereRaw('1 = 0'); // 返回空结果
|
|
} else {
|
|
// 获取这些审批单中当前用户待审批的,排除已审批的
|
|
$approvedOrderIds = ApprovalRecord::whereIn('order_id', $pendingOrderIds)
|
|
->where('approver_id', $userId)
|
|
->whereIn('action', ['approve', 'reject'])
|
|
->pluck('order_id')
|
|
->toArray();
|
|
|
|
$pendingOrderIds = array_diff($pendingOrderIds, $approvedOrderIds);
|
|
|
|
$query->whereIn('approval_order_id', $pendingOrderIds);
|
|
}
|
|
break;
|
|
case 'approved':
|
|
// 我已审批:当前用户已经审批过的申请
|
|
$approvedOrderIds = ApprovalRecord::where('approver_id', $userId)
|
|
->whereIn('action', ['approve', 'reject'])
|
|
->pluck('order_id')
|
|
->toArray();
|
|
|
|
if (empty($approvedOrderIds)) {
|
|
$query->whereRaw('1 = 0'); // 返回空结果
|
|
} else {
|
|
$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) {
|
|
BusinessTripItinerary::create([
|
|
'business_trip_id' => $businessTrip->id,
|
|
'start_time' => $itinerary['start_time'],
|
|
'end_time' => $itinerary['end_time'] ?? null,
|
|
'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();
|
|
}
|
|
}
|
|
|
|
return $businessTrip->fresh();
|
|
}
|
|
|
|
/**
|
|
* 删除草稿
|
|
*/
|
|
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
|
|
{
|
|
return $this->db->transaction(function () use ($userId, $businessTripId, $action, $comment) {
|
|
$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('当前状态无法审批');
|
|
}
|
|
|
|
// 检查用户是否是当前审批人
|
|
$currentNodes = $approvalOrder->current_nodes ?? [];
|
|
$isApprover = false;
|
|
|
|
foreach ($currentNodes as $nodeId) {
|
|
$node = ApprovalNode::where('flow_id', $approvalOrder->flow_id)
|
|
->where('id', $nodeId)
|
|
->first();
|
|
|
|
if (!$node) {
|
|
continue;
|
|
}
|
|
|
|
// 检查用户是否在审批人列表中
|
|
$approvers = $node->approvers ?? [];
|
|
if (in_array($userId, $approvers)) {
|
|
$isApprover = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$isApprover) {
|
|
throw new \Exception('您不是当前审批人');
|
|
}
|
|
|
|
// 检查是否已经审批过
|
|
$existingRecord = ApprovalRecord::where('order_id', $approvalOrder->id)
|
|
->where('approver_id', $userId)
|
|
->whereIn('action', ['approve', 'reject'])
|
|
->first();
|
|
|
|
if ($existingRecord) {
|
|
throw new \Exception('您已经审批过了');
|
|
}
|
|
|
|
// 记录审批操作
|
|
ApprovalRecord::create([
|
|
'order_id' => $approvalOrder->id,
|
|
'node_id' => $currentNodes[0] ?? 1,
|
|
'node_type' => 'approver',
|
|
'approver_id' => $userId,
|
|
'action' => $action,
|
|
'comment' => $comment,
|
|
]);
|
|
|
|
// 更新审批单状态
|
|
if ($action === 'reject') {
|
|
// 拒绝:直接结束流程
|
|
$approvalOrder->status = ApprovalOrder::STATUS_REJECTED;
|
|
$approvalOrder->current_nodes = [];
|
|
$businessTrip->status = BusinessTrip::STATUS_REJECTED;
|
|
} else {
|
|
// 同意:检查是否还有后续节点(简化版:直接通过)
|
|
$approvalOrder->status = ApprovalOrder::STATUS_APPROVED;
|
|
$approvalOrder->current_nodes = [];
|
|
$businessTrip->status = BusinessTrip::STATUS_APPROVED;
|
|
}
|
|
|
|
$approvalOrder->save();
|
|
$businessTrip->save();
|
|
|
|
return $businessTrip->fresh(['itineraries', 'approvalOrder']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 获取默认审批人和抄送人
|
|
* 从审批流程配置表中获取
|
|
*/
|
|
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,
|
|
'position' => $user->position,
|
|
];
|
|
}
|
|
}
|
|
|
|
// 审批节点(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,
|
|
];
|
|
}
|
|
}
|