From 2a448029948961cdf4f0e0f180d20e8004d735a6 Mon Sep 17 00:00:00 2001 From: mzl Date: Wed, 24 Sep 2025 17:38:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=A1=E6=89=B9=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E8=BD=B4&&=E8=B0=83=E6=95=B4=E9=87=87=E8=B4=AD?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApprovalRecordsController.php | 29 +++ .../controller/PurchaseOrdersController.php | 1 + app/common/model/PurchaseOrders.php | 2 +- app/common/service/ApprovalRecordsService.php | 17 +- app/common/service/PurchaseOrdersService.php | 120 ++++++----- .../approval/service/ApprovalFlowService.php | 189 +++++++++++++++++- config/route.php | 1 + 7 files changed, 299 insertions(+), 60 deletions(-) diff --git a/app/admin/controller/ApprovalRecordsController.php b/app/admin/controller/ApprovalRecordsController.php index 95bd2b6..27f9fb2 100644 --- a/app/admin/controller/ApprovalRecordsController.php +++ b/app/admin/controller/ApprovalRecordsController.php @@ -169,4 +169,33 @@ class ApprovalRecordsController extends BaseController return $this->error($e->getMessage()); } } + + + public function approvalTimeline(Request $request) + { + try { + $raw = $this->service->serviceApprovalTimeline($request, $request->all()); + $data = $this->transformData($raw, method_exists($this, 'timelineFormat') + ? [$this, 'timelineFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + protected function timelineFormat($data) + { + $result = []; + foreach ($data as $record) { + $result[] = [ + 'id' => $record['id'], + 'name' => $record['approver']['name'], + 'comment' => $record['comment'], + 'time' => $record['updated_at'] + ]; + } + return $result; + } } \ No newline at end of file diff --git a/app/common/controller/PurchaseOrdersController.php b/app/common/controller/PurchaseOrdersController.php index d880dab..5b86f47 100644 --- a/app/common/controller/PurchaseOrdersController.php +++ b/app/common/controller/PurchaseOrdersController.php @@ -43,6 +43,7 @@ class PurchaseOrdersController extends BaseController 'contract_file' => $contractFile, 'contract_file_url' => $contractFileUrl, 'status' => $record['status'], + 'status_text' => array_flip(\app\common\service\SysDictService::pluckKeyValueByCode('purchase_status'))[$record['status']], 'remark' => $record['remark'], 'created_at' => date('Y-m-d H:i:s', $record['created_at']), 'updated_at' => date('Y-m-d H:i:s', $record['updated_at']), diff --git a/app/common/model/PurchaseOrders.php b/app/common/model/PurchaseOrders.php index c03619e..f51349a 100644 --- a/app/common/model/PurchaseOrders.php +++ b/app/common/model/PurchaseOrders.php @@ -44,7 +44,7 @@ class PurchaseOrders extends Model /** * 允许自动填充模型字段 */ - protected $fillable = ['id', 'order_no', 'pi_no', 'buyer_id', 'buyer_name', 'contract_file', 'status', 'remark', 'created_at', 'updated_at']; + protected $fillable = ['id', 'order_no', 'pi_no', 'buyer_id', 'buyer_name', 'contract_file', 'status', 'remark', 'is_draft', 'created_at', 'updated_at']; /** * 不允许自动填充模型字段 diff --git a/app/common/service/ApprovalRecordsService.php b/app/common/service/ApprovalRecordsService.php index 2704ac8..3be5881 100644 --- a/app/common/service/ApprovalRecordsService.php +++ b/app/common/service/ApprovalRecordsService.php @@ -103,14 +103,7 @@ class ApprovalRecordsService 'approver', 'instance.flows', 'instance.initiator', - 'instance.bizInfo', - - // 'instance.bizInfo.items', - // 'instance.bizInfo.items.options', - // 'instance.bizInfo.items.moneyValues', - // 'instance.bizInfo.accounts', - // 'instance.bizInfo.moneyValues', - // 'instance.bizInfo.salesperson', + 'instance.bizInfo' ]; break; } @@ -178,4 +171,12 @@ class ApprovalRecordsService $result = $service->transferApproval($data['instance_id'], $data['step_index'], $request->user->uid, $data['to_uid'], $data['reason'], 'name'); return $result; } + + + public function serviceApprovalTimeline(Request $request, array $data = []) + { + $instance = \app\common\model\ApprovalInstances::where('biz_id', $data['id'])->where('flow_type', $data['flow_type'])->first(); + $result = \app\common\model\ApprovalRecords::where('instance_id', $instance->id)->with('approver')->orderBy('id', 'desc')->get(); + return $result; + } } \ No newline at end of file diff --git a/app/common/service/PurchaseOrdersService.php b/app/common/service/PurchaseOrdersService.php index 75b53a5..64ca27e 100644 --- a/app/common/service/PurchaseOrdersService.php +++ b/app/common/service/PurchaseOrdersService.php @@ -74,6 +74,9 @@ class PurchaseOrdersService $data['order_no'] = generateStockNo('CG'); $data['buyer_id'] = $request->user->uid; $data['buyer_name'] = User::where('id', $request->user->uid)->value('name'); + if ($data['is_draft'] == 0) { + $data['status'] = 1; + } return $data; } @@ -102,52 +105,6 @@ class PurchaseOrdersService }); } - /** - * 更新采购单 - * - * @param Request $request - * @param array $data - * @return mixed - */ - public function serviceUpdate(Request $request, array $data = [], string $scene = 'update') - { - // 使用事务确保数据一致性 - return Db::transaction(function () use ($request, $data, $scene) { - $data = $this->preProcess(ucfirst($scene), $data, $request); - - // 构建查询条件 - $config = $this->buildQueryConfig($request, $scene); - - // 更新主表信息 - $udata = $data; - unset($udata['stock_order']); - $updated = $this->logicUpdateBy($config, $udata); - - // 获取更新后的数据 - $order = $this->logicShow($config); - - // 更新采购单明细 - if (isset($data['stock_order'])) { - // 先删除原有的明细 - PurchaseOrderItems::where('purchase_order_id', $order->id)->delete(); - - // 重新创建明细 - foreach ($data['stock_order'] as $orderData) { - foreach ($orderData['stock_items'] as $item) { - PurchaseOrderItems::create([ - 'purchase_order_id' => $order->id, - 'stock_id' => $orderData['stock_orders_id'], - 'stock_items_id' => $item['stock_order_items_id'], - 'stock_items_uqid' => $item['unique_id'], - ]); - } - } - } - - return $this->postProcess(ucfirst($scene), $order, ['is_resubmit'=>$data['is_resubmit']]); - }); - } - /** * 构建查询配置 * @@ -213,7 +170,7 @@ class PurchaseOrdersService */ protected function afterServiceStore($res) { - if ($res->status == 1) { + if ($res->is_draft == 0) { $this->startApproval($res, ['resubmitted'=>0]); } return $res; @@ -225,6 +182,69 @@ class PurchaseOrdersService $appro->start('purchase_orders', 'purchase_orders', $item->id, ['resubmitted'=>0], $item->buyer_id); } + public function beforeServiceUpdate(array $data, Request $request) + { + if ($data['is_draft'] == 0) { + $data['status'] = 1; + } + return $data; + } + + /** + * 更新采购单 + * + * @param Request $request + * @param array $data + * @return mixed + */ + public function serviceUpdate(Request $request, array $data = [], string $scene = 'update') + { + // 使用事务确保数据一致性 + return Db::transaction(function () use ($request, $data, $scene) { + $data = $this->preProcess(ucfirst($scene), $data, $request); + + // 构建查询条件 + $config = $this->buildQueryConfig($request, $scene); + + // 更新主表信息 + $udata = $data; + unset($udata['stock_order']); + unset($udata['is_resubmit']); + unset($udata['approval_desc']); + $this->logicUpdateBy($config, $udata); + + // 获取更新后的数据 + $order = $this->logicShow($config); + + // 更新采购单明细 + if (isset($data['stock_order'])) { + // 先删除原有的明细 + PurchaseOrderItems::where('purchase_order_id', $order->id)->delete(); + + // 重新创建明细 + foreach ($data['stock_order'] as $orderData) { + foreach ($orderData['stock_items'] as $item) { + PurchaseOrderItems::create([ + 'purchase_order_id' => $order->id, + 'stock_id' => $orderData['stock_orders_id'], + 'stock_items_id' => $item['stock_order_items_id'], + 'stock_items_uqid' => $item['unique_id'], + ]); + } + } + } + + return $this->postProcess( + ucfirst($scene), + $order, + [ + 'is_resubmit'=>$data['is_resubmit'], + 'approval_desc'=>$data['approval_desc'] + ] + ); + }); + } + protected function afterServiceUpdate($res, $data) { if ($data['is_resubmit'] == 0 && $res->status == 1) { @@ -238,10 +258,10 @@ class PurchaseOrdersService // 审批服务 $appro = new \app\common\utils\approval\service\ApprovalFlowService(); // 重新提交 - $appro->resubmit($instance->id, $instance->initiator_id, ['resubmitted'=>1], $data['remark']); + $appro->resubmit($instance->id, $instance->initiator_id, ['resubmitted'=>1], $data['approval_desc']); // 添加到讨论 - if (!empty($data['remark'])) { - $appro->postDiscussion($instance->id, $instance->initiator_id, $data['remark'], false, [], 'name'); + if (!empty($data['approval_desc'])) { + $appro->postDiscussion($instance->id, $instance->initiator_id, $data['approval_desc'], false, [], 'name'); } } return $res; diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index a97e5ea..6241057 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -808,7 +808,7 @@ class ApprovalFlowService * initiator_name?:?string * } */ - public function getSimpleTimeline(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array + public function getSimpleTimeline2(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array { // 基础数据 $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); @@ -958,6 +958,193 @@ class ApprovalFlowService } + public function getSimpleTimeline(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array + { + // 基础数据 + $instance = Db::table('approval_instances')->where('id', $instanceId)->first(); + if (!$instance) throw new BusinessException('审批实例未找到'); + + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) throw new BusinessException('审批流程定义未找到'); + + $steps = json_decode($flow->steps, true) ?: []; + $formData = json_decode($instance->form_data ?? '{}', true) ?: []; + $curStep = (int)$instance->current_step; + $round = (int)$instance->round; + + // 申请人 + $initiatorId = (int)$instance->initiator_id; + $initiatorName = null; + if ($userNameColumn) { + $initiatorName = Db::table('user')->where('id', $initiatorId)->value($userNameColumn); + } + + // 审批记录(仅当前轮次) + $records = Db::table('approval_records as ar') + ->where('ar.instance_id', $instanceId) + ->where('ar.round', $round) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + + // 一次性拉取姓名,避免 N+1 + $nameMap = []; + if ($userNameColumn && !empty($records)) { + $uids = []; + foreach ($records as $r) $uids[] = (int)$r->approver_id; + $uids = array_values(array_unique($uids)); + if (!empty($uids)) { + $pairs = Db::table('user')->whereIn('id', $uids)->pluck($userNameColumn, 'id')->toArray(); + foreach ($pairs as $uid => $name) $nameMap[(int)$uid] = $name; + } + } + + // 记录分组 + $byStep = []; + foreach ($records as $r) { + $idx = (int)$r->step_index; + $byStep[$idx][] = $r; + } + + $timeline = []; + + // 可选:发起申请块 + if ($initiatorPlacement === 'prepend') { + $appliedAt = $instance->created_at ?: null; + $timeline[] = [ + 'index' => -1, + 'name' => '发起申请', + 'type' => '-', + 'status' => 'submitted', + 'started_at' => $appliedAt, + 'finished_at' => $appliedAt, + 'duration_seconds' => 0, + 'initiator_id' => $initiatorId, + 'initiator_name' => $initiatorName, + ]; + } + + // 每一步 + foreach ($steps as $i => $meta) { + $name = $meta['name'] ?? ('Step '.$i); + $typeRaw = strtolower($meta['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + $condStr = $meta['condition'] ?? null; + $condOk = $condStr ? $this->evaluateCondition($condStr, $formData) : true; + + $recs = $byStep[$i] ?? []; + $hasRecs = !empty($recs); + + $status = 'pending'; + $startedAt = null; + $finishedAt = null; + $durationSec = null; + + // ★ 每一步的“审批意见”(只展示有内容的 comment) + $opinions = []; // [{approver_id, approver_name?, action, status, comment, time}...] + + if ($hasRecs) { + // 统计时间 + $minCreated = null; + $minApproveAt = null; $maxApproveAt = null; + $minRejectAt = null; + $maxTimeoutAt = null; + $cnt = ['pending'=>0,'approved'=>0,'rejected'=>0,'timeout'=>0]; + + foreach ($recs as $rr) { + // 收集“审批意见”(非空 comment 都算) + $cmt = trim((string)($rr->comment ?? '')); + if ($cmt !== '') { + $opinions[] = [ + 'approver_id' => (int)$rr->approver_id, + 'approver_name' => $userNameColumn ? ($nameMap[(int)$rr->approver_id] ?? null) : null, + 'action' => $rr->action, // approve | reject | null + 'status' => $rr->status, // approved|rejected|pending|skipped|timeout + 'comment' => $cmt, + 'time' => $rr->approved_at ?: $rr->created_at, + ]; + } + + if ($rr->created_at && (!$minCreated || $rr->created_at < $minCreated)) $minCreated = $rr->created_at; + if ($rr->approved_at) { + if ($rr->status === 'approved') { + if (!$minApproveAt || $rr->approved_at < $minApproveAt) $minApproveAt = $rr->approved_at; + if (!$maxApproveAt || $rr->approved_at > $maxApproveAt) $maxApproveAt = $rr->approved_at; + } elseif ($rr->status === 'rejected') { + if (!$minRejectAt || $rr->approved_at < $minRejectAt) $minRejectAt = $rr->approved_at; + } elseif ($rr->status === 'timeout') { + if (!$maxTimeoutAt || $rr->approved_at > $maxTimeoutAt) $maxTimeoutAt = $rr->approved_at; + } + } + if (isset($cnt[$rr->status])) $cnt[$rr->status]++; + } + + // 时间升序排序 opinions + if (!empty($opinions)) { + usort($opinions, function($a, $b) { + return strcmp($a['time'] ?? '', $b['time'] ?? ''); + }); + } + + $startedAt = $minCreated; + + // 总结果(注意:这里按你现在的签名调用,含 round) + $outcome = $this->getStepOutcome($instanceId, $round, $i, $type); // 'pending'|'approve'|'reject' + if ($outcome === 'reject') { + $status = 'rejected'; + $finishedAt = $minRejectAt; + } elseif ($outcome === 'approve') { + $status = 'approved'; + $finishedAt = ($type === 'any') ? $minApproveAt : $maxApproveAt; + } else { + if ($cnt['timeout'] > 0 && $cnt['approved'] === 0 && $cnt['rejected'] === 0 && $cnt['pending'] === 0) { + $status = 'timeout'; + $finishedAt = $maxTimeoutAt; + } else { + $status = 'processing'; + } + } + + if ($startedAt && $finishedAt) { + $durationSec = max(0, strtotime($finishedAt) - strtotime($startedAt)); + } + } else { + // 无记录:根据位置与条件展示 + if ($i < $curStep) { + $status = $condOk ? 'approved' : 'skipped'; + } elseif ($i === $curStep && $instance->status === 'processing') { + $status = 'processing'; + } else { + $status = $condOk ? 'pending' : 'skipped'; + } + } + + $row = [ + 'index' => (int)$i, + 'name' => $name, + 'type' => $type, + 'status' => $status, + 'started_at' => $startedAt, + 'finished_at' => $finishedAt, + 'duration_seconds' => $durationSec, + 'opinions' => $opinions, // ★ 这就是每个审批意见 + ]; + + // 把申请人信息并到第一步(可选) + if ($initiatorPlacement === 'attach' && empty($timeline)) { + $row['initiator_id'] = $initiatorId; + $row['initiator_name'] = $initiatorName; + } + + $timeline[] = $row; + } + + return $timeline; + } + + /** * 按业务ID获取“最新进展”的极简状态 * - 已完结:直接返回实例最终状态 diff --git a/config/route.php b/config/route.php index 4f26a64..4049182 100644 --- a/config/route.php +++ b/config/route.php @@ -319,6 +319,7 @@ Route::group('/admin', function () { Route::any('/todo', [app\admin\controller\ApprovalRecordsController::class, 'todo']); Route::any('/border', [app\admin\controller\ApprovalRecordsController::class, 'businessOrder']); Route::any('/transfer', [app\admin\controller\ApprovalRecordsController::class, 'transferApproval']); + Route::any('/timeline', [app\admin\controller\ApprovalRecordsController::class, 'approvalTimeline']); }); Route::group('/stock/order', function () {