diff --git a/app/admin/controller/ApprovalRecordsController.php b/app/admin/controller/ApprovalRecordsController.php index 46712b3..4e963ea 100644 --- a/app/admin/controller/ApprovalRecordsController.php +++ b/app/admin/controller/ApprovalRecordsController.php @@ -72,4 +72,17 @@ class ApprovalRecordsController extends BaseController return $this->error($e->getMessage()); } } + + + public function logs(Request $request) + { + try { + $instanceId = $request->get('instance_id'); + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $res = $appro->getSimpleTimeline($instanceId, 'name', 'prepend'); + return $this->success($res); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } } \ No newline at end of file diff --git a/app/admin/service/ApprovalRecordsService.php b/app/admin/service/ApprovalRecordsService.php index 0bc6325..1cad6b4 100644 --- a/app/admin/service/ApprovalRecordsService.php +++ b/app/admin/service/ApprovalRecordsService.php @@ -48,6 +48,10 @@ class ApprovalRecordsService 'approvalInstance.initiator', 'approvalInstance.bizInfo' ]; + + $config['dataLimit']['type'] = 'relation'; + $config['dataLimit']['userId'] = $request->user->uid; + $config['dataLimit']['userField'] = 'approver_id'; break; case 'show': $config['where']['id'] = $p['id']; @@ -60,6 +64,10 @@ class ApprovalRecordsService case 'destroy': $config['where']['id'] = $p['id']; break; + case 'logs': + $config['where']['instance_id'] = $p['instance_id']; + $config['order']['id'] = 'desc'; + break; } return $config; @@ -72,4 +80,12 @@ class ApprovalRecordsService $appro = new \app\common\utils\approval\service\ApprovalFlowService(); return $appro->approve($param['instance_id'], $request->user->uid, $param['action'], $param['comment']); } + + + public function serviceLogs(Request $request) + { + $config = $this->buildQueryConfig($request, 'logs'); + $item = $this->logicListAll($config); + return $this->postProcess('Logs', $item); + } } \ No newline at end of file diff --git a/app/common/model/StockOrders.php b/app/common/model/StockOrders.php index 815783e..f91191b 100644 --- a/app/common/model/StockOrders.php +++ b/app/common/model/StockOrders.php @@ -50,7 +50,7 @@ class StockOrders extends Model 'trade_term_id', 'trade_term_snapshot', 'clearance_port_id', 'clearance_port_snapshot', 'latest_arrival_date', 'contract_exchange_rate', 'settlement_currency', 'status', 'remark', 'created_at', 'updated_at', 'deleted_at', - 'object_id', 'contract', 'is_use', 'is_final', 'approval_status' + 'object_id', 'contract', 'is_use', 'is_final', 'approval_status', 'file_url' ]; /** diff --git a/app/common/service/StockOrdersService.php b/app/common/service/StockOrdersService.php index fadd6cc..6a6d6b6 100644 --- a/app/common/service/StockOrdersService.php +++ b/app/common/service/StockOrdersService.php @@ -64,9 +64,11 @@ class StockOrdersService $config['order']['id'] = 'desc'; break; - case 'show': case 'print': $config['where']['id'] = $p['id']; + break; + case 'show': + $config['where']['id'] = $p['id']; $config['with'][] = 'items'; $config['with'][] = 'items.options'; @@ -224,13 +226,24 @@ class StockOrdersService $appro = new \app\common\utils\approval\service\ApprovalFlowService(); $result = $appro->start('stock_order_flow', 'stock_orders', $res->id, $res->toArray(), $res->salesperson_id); var_dump($result); + + // 异步生成pdf文件 + \Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$res->id]); return $res; } - public function servicePrint(Request $request) + public function createdPdfFile($id) { - $config = $this->buildQueryConfig($request, 'print'); + $config = []; + $config['model'] = $this->modelClass; + $config['where']['id'] = $id; + $config['with'][] = 'items'; + $config['with'][] = 'items.options'; + $config['with'][] = 'items.moneyValues'; + $config['with'][] = 'accounts'; + $config['with'][] = 'moneyValues'; + $config['with'][] = 'salesperson'; $item = $this->logicShow($config); $pdfData = $this->pdfData($item); @@ -259,11 +272,35 @@ class StockOrdersService // 'Content-Disposition' => 'inline; filename="stock-order.pdf"', // ], $pdfBin); - $url = [ - 'file_name' => $fileName, - 'file_url' => getenv('MAIN_DOMAIN').'/pdf/'.$fileName + // $url = [ + // 'file_name' => $fileName, + // 'file_url' => getenv('MAIN_DOMAIN').'/pdf/'.$fileName + // ]; + + // 上传云端 + $param = [ + 'path' => $dir . $fileName, + 'scene' => 'stock_orders', + 'category' => 'pdf', + 'original_name' => $fileName ]; - return $url; + $up = new UploadService(); + $res = $up->uploadLocalFile($param); + + @unlink($dir . $fileName); + + // 更新文件地址 + $item->file_url = $res['file_name']; + $item->save(); + + return $res; + } + + public function servicePrint(Request $request) + { + $config = $this->buildQueryConfig($request, 'show'); + $item = $this->logicShow($config); + return $this->postProcess('Print', $item); } diff --git a/app/common/service/UploadService.php b/app/common/service/UploadService.php index 74193d5..d38bba6 100644 --- a/app/common/service/UploadService.php +++ b/app/common/service/UploadService.php @@ -50,7 +50,7 @@ class UploadService $size = filesize($filePath) ?: 0; $sceneUrl = $scene ? ($scene . '/') : ''; - $dir = 'vehicle_album/' . $sceneUrl . date('Ymd') . '/'; + $dir = 'x_server/' . $sceneUrl . date('Ymd') . '/'; $uniqueName = md5($originalName . microtime(true)); $objectKey = $dir . $uniqueName . ($extension ? ('.' . $extension) : ''); diff --git a/app/common/utils/approval/service/ApprovalFlowService.php b/app/common/utils/approval/service/ApprovalFlowService.php index 0e32dfb..2adf96b 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -752,4 +752,269 @@ class ApprovalFlowService 'summary' => $summary, ]; } + + /** + * 极简:审批实例的时间轴(只返回每一步的总状态) + * + * @param int $instanceId + * @return array + */ + public function getSimpleTimeline1(int $instanceId): 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) ?: []; + $currentStep = (int)$instance->current_step; + + // 读出所有记录并分组 + $records = Db::table('approval_records as ar') + ->where('ar.instance_id', $instanceId) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + + $byStep = []; + foreach ($records as $r) { + $idx = (int)$r->step_index; + $byStep[$idx][] = $r; + } + + $timeline = []; + + 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'; + + if ($hasRecs) { + // 统计是否全部 timeout(且无 approve/reject/pending) + $cnt = ['pending'=>0,'approved'=>0,'rejected'=>0,'timeout'=>0]; + foreach ($recs as $rr) { + if (isset($cnt[$rr->status])) $cnt[$rr->status]++; + } + + // 用已有的聚合规则拿到结论 + $outcome = $this->getStepOutcome($instanceId, $i, $type); // 'pending'|'approve'|'reject' + + if ($outcome === 'reject') { + $status = 'rejected'; + } elseif ($outcome === 'approve') { + $status = 'approved'; + } else { + // 没有结论,再看是不是纯超时 + if ($cnt['timeout'] > 0 && $cnt['approved'] === 0 && $cnt['rejected'] === 0 && $cnt['pending'] === 0) { + $status = 'timeout'; + } else { + $status = 'processing'; + } + } + } else { + // 无记录:根据位置与条件判断展示 + if ($i < $currentStep) { + $status = $condOk ? 'approved' : 'skipped'; + } elseif ($i === $currentStep && $instance->status === 'processing') { + // 理论上当前步应有记录;防御性展示为 processing + $status = 'processing'; + } else { + $status = $condOk ? 'pending' : 'skipped'; + } + } + + $timeline[] = [ + 'index' => (int)$i, + 'name' => $name, + 'type' => $type, // any | all + 'status' => $status, // approved | rejected | processing | pending | skipped | timeout + ]; + } + + return $timeline; + } + + + /** + * 极简时间轴(含每步时间 & 申请人信息) + * + * @param int $instanceId + * @param string|null $userNameColumn 申请人姓名列(如 'name' / 'nickname')。不需要就传 null + * @param string $initiatorPlacement 'prepend' | 'attach' + * - 'prepend':在最前插入一条“发起申请”块(推荐) + * - 'attach' :把申请人信息并到第一步的返回项里(不新增块) + * + * 返回:按显示顺序的数组,每项: + * { + * index:int // 步骤索引;若为“发起申请”块则为 -1 + * name:string // 步骤名或“发起申请” + * type:string // any|all 或 '-'(发起块) + * status:string // approved|rejected|processing|pending|skipped|timeout|submitted(发起块) + * started_at:?string + * finished_at:?string + * duration_seconds:?int + * // 仅发起块或 attach 时的第一步会包含: + * initiator_id?:int + * initiator_name?:?string + * } + */ + 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; + + // 申请人 + $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) + ->orderBy('ar.step_index') + ->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc') + ->select('ar.*') + ->get() + ->toArray(); + + $byStep = []; + foreach ($records as $r) { + $idx = (int)$r->step_index; + $byStep[$idx][] = $r; + } + + $timeline = []; + + // 可选:先插入“发起申请”块(index = -1) + 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; + + 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) { + 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]++; + } + + $startedAt = $minCreated; + + // 总结果 + $outcome = $this->getStepOutcome($instanceId, $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, + ]; + + // 如果需要把申请人信息“并到第一步”而不是独立一块 + if ($initiatorPlacement === 'attach' && empty($timeline)) { + $row['initiator_id'] = $initiatorId; + $row['initiator_name'] = $initiatorName; + } + + $timeline[] = $row; + } + + return $timeline; + } + } \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index f7d924d..54ffb3d 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -2,6 +2,7 @@ namespace app\controller; +use app\common\service\UploadService; use app\common\utils\scaffold\Generate; use support\Request; use support\Db; @@ -99,21 +100,32 @@ class IndexController { - $flowType = 'stock_order_flow'; - $bizType = 'stock_orders'; - $bizId = 32; - $formData = []; - $initiatorId = 74; + // $flowType = 'stock_order_flow'; + // $bizType = 'stock_orders'; + // $bizId = 32; + // $formData = []; + // $initiatorId = 74; - $appro = new \app\common\utils\approval\service\ApprovalFlowService(); - $sp = $appro->start($flowType, $bizType, $bizId, $formData, $initiatorId); + // $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + // // $sp = $appro->start($flowType, $bizType, $bizId, $formData, $initiatorId); - // $sp = $appro->approve(9, 1, 'reject'); - var_dump($sp); + // $sp = $appro->approve(11, 1, 'reject'); + // var_dump($sp); - return json([ - 'code' => 0, - 'msg' => 'success' - ]); + // return json([ + // 'code' => 0, + // 'msg' => 'success' + // ]); + + // $param = [ + // 'path' => public_path().'/pdf/BH20250908014.pdf', + // 'scene' => 'stock_orders', + // 'category' => 'pdf', + // 'original_name' => 'BH20250908014.pdf' + // ]; + + // $up = new UploadService(); + // $res = $up->uploadLocalFile($param); + // var_dump($res); } } \ No newline at end of file diff --git a/app/crm/controller/StockOrdersController.php b/app/crm/controller/StockOrdersController.php index 0930a1a..75fe8fc 100644 --- a/app/crm/controller/StockOrdersController.php +++ b/app/crm/controller/StockOrdersController.php @@ -107,13 +107,22 @@ class StockOrdersController extends BaseController return $data; } + + protected function printFormat($data) + { + $data['file_url_full'] = (!empty($data['file_url'])) ? (new \app\common\service\OssService)->generatePrivateUrl($data['file_url']) : ''; + return $data; + } public function print(Request $request) { try { $raw = $this->service->servicePrint($request); - return $this->success($raw); + $data = $this->transformData($raw, method_exists($this, 'printFormat') + ? [$this, 'printFormat'] + : null); + return $this->success($data); } catch (\Throwable $e) { return $this->error($e->getMessage()); } diff --git a/app/queue/redis/StockOrderPdf.php b/app/queue/redis/StockOrderPdf.php new file mode 100644 index 0000000..5c75a7e --- /dev/null +++ b/app/queue/redis/StockOrderPdf.php @@ -0,0 +1,51 @@ +createdPdfFile($data['id']); + + return true; + } catch (\Throwable $th) { + var_dump('4 :'.$th->getFile()."|".$th->getLine()."|".$th->getMessage()); + throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage()); + } + } + + // 消费失败回调 + /* + $package = [ + 'id' => 1357277951, // 消息ID + 'time' => 1709170510, // 消息时间 + 'delay' => 0, // 延迟时间 + 'attempts' => 2, // 消费次数 + 'queue' => 'send-mail', // 队列名 + 'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容 + 'max_attempts' => 5, // 最大重试次数 + 'error' => '错误信息' // 错误信息 + ] + */ + public function onConsumeFailure(\Throwable $e, $package) + { + echo "consume failure\n"; + echo $e->getMessage() . "\n"; + // 无需反序列化 + var_export($package); + } +} \ No newline at end of file diff --git a/config/route.php b/config/route.php index 8f693e3..afc379f 100644 --- a/config/route.php +++ b/config/route.php @@ -302,10 +302,10 @@ Route::group('/admin', function () { // 审批记录 Route::group('/approval/records', function () { Route::any('/list', [app\admin\controller\ApprovalRecordsController::class, 'index']); - Route::any('/show', [app\admin\controller\ApprovalRecordsController::class, 'show']); - Route::any('/store', [app\admin\controller\ApprovalRecordsController::class, 'store']); + Route::any('/logs', [app\admin\controller\ApprovalRecordsController::class, 'logs']); + // Route::any('/store', [app\admin\controller\ApprovalRecordsController::class, 'store']); Route::any('/action', [app\admin\controller\ApprovalRecordsController::class, 'approve']); - Route::any('/destroy', [app\admin\controller\ApprovalRecordsController::class, 'destroy']); + // Route::any('/destroy', [app\admin\controller\ApprovalRecordsController::class, 'destroy']); }); }); diff --git a/public/pdf/BH20250908014.pdf b/public/pdf/BH20250908014.pdf deleted file mode 100644 index 9b4b9bd..0000000 Binary files a/public/pdf/BH20250908014.pdf and /dev/null differ