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/controller/StockOrdersController.php b/app/admin/controller/StockOrdersController.php new file mode 100644 index 0000000..128fc19 --- /dev/null +++ b/app/admin/controller/StockOrdersController.php @@ -0,0 +1,240 @@ +service = new StockOrdersService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']); + if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']); + if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']); + + if (isset($data['id'])) { + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro = $appro->getBriefStatusByBiz($data['id'], null, 'name'); + $map = ['processing'=>'审批中','pending'=>'审批中','approved'=>'已通过','rejected'=>'已拒绝','revoked'=>'已撤回']; + if ($appro) { + $data['appro_status'] = [ + 'status' => $appro['status'], + 'status_desc' => $map[$appro['status']], + 'name' => isset($appro['step'])?$appro['step']['name']:'' + ]; + } + } + return $data; + } + + + protected function showFormat($data) + { + if (isset($data['created_at'])) $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']); + if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']); + if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']); + + if (!empty($data['contract'])) { + $data['contract_url'] = (new \app\common\service\OssService)->generatePrivateUrl($data['contract']); + } + + if (count($data['moneyValues']) > 0) { + $data['cif_total_amount'] = [ + 'exchange_rate' => $data['moneyValues']['exchange_rate'], + 'amount' => $data['moneyValues']['amount'], + 'currency' => $data['moneyValues']['currency'], + 'id' => $data['moneyValues']['id'] + ]; + } + + foreach ($data['items'] as &$item) { + foreach ($item['moneyValues'] as $mv) { + $item[$mv['field']] = [ + 'exchange_rate' => $mv['exchange_rate'], + 'amount' => $mv['amount'], + 'currency' => $mv['currency'], + 'id' => $mv['id'] + ]; + } + unset($item['moneyValues']); + + foreach ($item['options'] as $opt) { + $item['modify_list'][] = [ + "id" => $opt['id'], + "order_item_id" => $opt['order_item_id'], + "option_id" => $opt['option_id'], + "option_name_snapshot" => $opt['option_name_snapshot'], + "option_price" => $opt['option_price'] + ]; + } + unset($item['options']); + } + unset($item); + + + foreach ($data['accounts'] as $acc) { + $data['account_list'][] = [ + "id" => $acc['id'], + "order_id" => $acc['order_id'], + "seq_no" => $acc['seq_no'], + "remit_country_id" => $acc['remit_country_id'], + "remit_country_name_snapshot" => $acc['remit_country_name_snapshot'], + "remit_date" => date('Y-m-d', $acc['remit_date']), + "currency_code" => $acc['currency_code'], + "currency_name_snapshot" => $acc['currency_name_snapshot'], + "account_id" => $acc['account_id'], + "account_name_snapshot" => $acc['account_name_snapshot'], + "remitter_name" => $acc['remitter_name'], + "amount" => $acc['amount'] + ]; + } + unset($data['accounts']); + + $data['veh_list'] = $data['items']; + unset($data['items']); + 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); + $data = $this->transformData($raw, method_exists($this, 'printFormat') + ? [$this, 'printFormat'] + : null); + return $this->success($data); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + + + public function desc(Request $request) + { + + $data = [ + 'id' => '', //'主键', + 'order_no' => '', //'备货单编号(业务唯一)', + 'pi_no' => '', //'PI号'/'阿里订单号', + 'biz_date' => '', //'单据日期(时间戳)', + 'salesperson_id' => '', //'业务员ID(关联数据)', + 'salesperson_snapshot' => '', //'业务员姓名快照', + 'clearance_country_id' => '', //'清关目的国ID(关联数据)', + 'clearance_country_snapshot' => '', //'清关目的国名称快照', + // 'ali_order_no' => '', //'阿里订单号', + 'customer_source_id' => '', //'客户来源ID(关联数据)', + 'customer_source_snapshot' => '', //'客户来源名称快照', + 'customer_id' => '', //'客户ID(关联数据)', + 'customer_snapshot' => '', //'客户名称快照', + 'customer_country_id' => '', //'客户所在国ID(关联数据)', + 'customer_country_snapshot' => '', //'客户所在国名称快照', + 'declarant_name' => '', //'清关人(文本)', + 'eta_ship_date' => '', //'预计发运日(时间戳)', + 'shipping_method' => '', //'出运方式(如海运/空运/铁路等)', + 'cif_total_amount' => '', //'0.00' COMMENT '合计到岸价(CIF)', + 'domestic_port_id' => '', //'国内收货港ID(关联数据)', + 'domestic_port_snapshot' => '', //'国内收货港名称快照', + 'trade_term_id' => '', //'贸易条款ID(关联数据,EXW/FOB/CIF等)', + 'trade_term_snapshot' => '', //'贸易条款名称快照', + 'clearance_port_id' => '', //'清关目的港ID(关联数据)', + 'clearance_port_snapshot' => '', //'清关目的港名称快照', + 'latest_arrival_date' => '', //'最迟到港时间(时间戳)', + 'contract_exchange_rate' => '', //'合同汇率(下单锁定)', + 'settlement_currency' => '', //'CNY' COMMENT '结算币种', + 'remark' => '', //'备注', + // 'real_veh' => '', //'0' COMMENT '实车ID', + 'is_final' => '', //'0' COMMENT '是否终版1是0否', + 'status' => '', //'draft' COMMENT '单据状态:draft/submitted/approved/void/reversed等', + ]; + + // (?<=\x60 )[^\r\n']*(?=') + + $items = [ + // 'id' => '', // '主键', + // 'order_id' => '', // '所属备货单ID', + // 'product_code' => '', // '商品编号(可作外部编码)', + // 'sku_snapshot' => '', // '商品/车型名称快照', + // 'unit_price_value_id' => '', // '单价', + // 'handling_fee_value_id' => '', // '操作费', + // 'fob_transport_fee_value_id' => '', // 'FOB内陆运费', + // 'port_misc_fee_value_id' => '', // '港杂费', + // 'retrofit_fee_value_id' => '', // '改装费', + // 'total_price_value_id' => '', // '合计(按订单结算币计算)', + + 'vin' => '', // '车架号', + 'sku_id' => '', // '商品/车型ID(关联数据)', + 'model_snapshot' => '', // '车型(若与sku一致可复用;此处为显示快照)', + 'quantity' => '', // '1.000000' COMMENT '数量', + 'color_interior_id' => '', // '颜色内饰ID(关联数据)', + 'color_interior_snapshot' => '', // '颜色内饰名称快照', + 'color_exterior_id' => '', // '颜色外饰ID(关联数据)', + 'color_exterior_snapshot' => '', // '颜色外饰名称快照', + 'remark' => '', // '备注', + 'unit_price' => '', // '0.000000' COMMENT '单价(含/不含税按业务口径约定)', + 'handling_fee' => '', // '0.000000' COMMENT '手续费(按单车)', + 'fob_transport_fee' => '', // '0.000000' COMMENT 'FOB运输费(按单车)', + 'port_misc_fee' => '', // '0.000000' COMMENT '港杂(按单车)', + 'retrofit_fee' => '', // '0.000000' COMMENT '改装费(按单车)', + 'total_price' => '', // '0.000000' COMMENT '总价(quantity*unit_price,写时落地)', + 'official_price' => '', // 官方指导价 + 'discount_price' => '', // 优惠价 + + + 'modify_list' => [ + // 'order_item_id' => '', // '所属明细ID', + 'option_id' => '', // '配置项ID(关联数据)', + 'option_name_snapshot' => '', // '配置项名称快照', + 'option_price' => '', // '配置项价格(可选,写时快照)', + ] + ]; + + + $account = [ + 'order_id' => '', // '所属备货单ID', + // 'seq_no' => '', // '1' COMMENT '账户序号(行号)', + 'remit_country_id' => '', // '汇款国ID(关联数据)', + 'remit_country_name_snapshot' => '', // '汇款国名称快照', + 'remit_date' => '', // '汇款日期(时间戳)', + 'currency_code' => '', // 'USD' COMMENT '币种代码(如USD/CNY/AED等)', + 'currency_name_snapshot' => '', // '币种名称快照(可选)', + 'account_id' => '', // '账户ID(关联数据)', + 'account_name_snapshot' => '', // '账户名称/开户名快照', + 'remitter_name' => '', // '汇款人', + 'amount' => '', // '0.00' COMMENT '金额', + ]; + } +} \ No newline at end of file diff --git a/app/admin/service/ApprovalRecordsService.php b/app/admin/service/ApprovalRecordsService.php index 0bc6325..a371f41 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 bec4942..2d9ac53 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..b794532 100644 --- a/app/common/utils/approval/service/ApprovalFlowService.php +++ b/app/common/utils/approval/service/ApprovalFlowService.php @@ -752,4 +752,390 @@ 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; + } + + + /** + * 按业务ID获取“最新进展”的极简状态 + * - 已完结:直接返回实例最终状态 + * - 进行中:返回当前步骤(索引、名称、类型)与当前待处理审批者列表(pending) + * + * @param string $bizId + * @param string|null $bizType 可选限定业务类型 + * @param string|null $userNameColumn 可选:审批人姓名字段(如 user.name / nickname) + * @return array{ + * instance_id:int, flow_type:string, biz_type:string, biz_id:string, + * status:string, // 实例状态:approved|rejected|revoked|processing + * finished_at:?string, updated_at:?string, + * step?: array{ + * index:int, name:string, type:string, // any|all + * status:string, // processing|timeout(进行中场景) + * pending_approvers: array + * } + * } + */ + public function getBriefStatusByBiz(string $bizId, ?string $bizType = null, ?string $userNameColumn = null): array + { + // 1) 最新实例 + $q = Db::table('approval_instances')->where('biz_id', $bizId); + if ($bizType !== null && $bizType !== '') { + $q->where('biz_type', $bizType); + } + $instance = $q->orderByDesc('id')->first(); + if (!$instance) { + // throw new BusinessException('未找到对应的审批实例'); + return []; + } + + $base = [ + 'instance_id' => (int)$instance->id, + 'flow_type' => $instance->flow_type, + 'biz_type' => $instance->biz_type, + 'biz_id' => $instance->biz_id, + 'status' => $instance->status, + 'finished_at' => $instance->finished_at, + 'updated_at' => $instance->updated_at, + ]; + + // 2) 已完结:直接返回 + if ($instance->status !== 'processing') { + return $base; + } + + // 3) 进行中:取当前步骤 + 当前 pending 审批者 + $flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first(); + if (!$flow) { + // 容错:没有流程定义 + return $base; + } + $steps = json_decode($flow->steps, true) ?: []; + $idx = (int)$instance->current_step; + $meta = $steps[$idx] ?? []; + + $typeRaw = strtolower($meta['type'] ?? 'all'); + $type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all'; + + // 当前步的所有记录 + $recsQ = Db::table('approval_records')->where('instance_id', $instance->id)->where('step_index', $idx); + + // 待处理人(只列出 pending) + if ($userNameColumn) { + $pendingRows = Db::table('approval_records as ar') + ->leftJoin('user as u', 'ar.approver_id', '=', 'u.id') + ->where('ar.instance_id', $instance->id) + ->where('ar.step_index', $idx) + ->where('ar.status', 'pending') + ->select('ar.approver_id', Db::raw("u.`{$userNameColumn}` as approver_name")) + ->get() + ->toArray(); + } else { + $pendingRows = Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $idx) + ->where('status', 'pending') + ->select('approver_id') + ->get() + ->toArray(); + } + + $pendingApprovers = []; + foreach ($pendingRows as $r) { + $row = ['approver_id' => (int)$r->approver_id]; + if ($userNameColumn && property_exists($r, 'approver_name')) { + $row['approver_name'] = $r->approver_name; + } + $pendingApprovers[] = $row; + } + + // 当前步的聚合状态(进行中通常返回 processing;若全被超时任务置为 timeout,则返回 timeout) + // 先快速统计是否纯 timeout + $stats = Db::table('approval_records') + ->where('instance_id', $instance->id) + ->where('step_index', $idx) + ->selectRaw("SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) as c_pending") + ->selectRaw("SUM(CASE WHEN status='approved' THEN 1 ELSE 0 END) as c_approved") + ->selectRaw("SUM(CASE WHEN status='rejected' THEN 1 ELSE 0 END) as c_rejected") + ->selectRaw("SUM(CASE WHEN status='timeout' THEN 1 ELSE 0 END) as c_timeout") + ->first(); + + $stepStatus = 'processing'; + if ($stats && (int)$stats->c_timeout > 0 && (int)$stats->c_pending === 0 && (int)$stats->c_approved === 0 && (int)$stats->c_rejected === 0) { + $stepStatus = 'timeout'; + } + + $base['step'] = [ + 'index' => $idx, + 'name' => $meta['name'] ?? ('Step '.$idx), + 'type' => $type, // any|all + 'status' => $stepStatus, // processing|timeout + 'pending_approvers' => $pendingApprovers, // 当前正在等待的审批者 + ]; + + return $base; + } + + } \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index f7d924d..28cb1d0 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; @@ -68,26 +69,26 @@ class IndexController { $options = [ 'controller' => [ - 'module' => 'admin', - 'name' => 'approval_instances' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'model' => [ - 'module' => 'admin', - 'name' => 'approval_instances' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'service' => [ - 'module' => 'admin', - 'name' => 'approval_instances' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'logic' => [ - 'module' => 'admin', - 'name' => 'approval_instances' + 'module' => 'crm', + 'name' => 'approval_flows' ], 'validate' => [ - 'module' => 'admin', - 'name' => 'approval_instances' + 'module' => 'crm', + 'name' => 'approval_flows' ], - 'table' => 'approval_instances' + 'table' => 'approval_flows' ]; $result = (new Generate($options))->doGenerate(); @@ -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/ApprovalFlowsController.php b/app/crm/controller/ApprovalFlowsController.php new file mode 100644 index 0000000..7eacb12 --- /dev/null +++ b/app/crm/controller/ApprovalFlowsController.php @@ -0,0 +1,33 @@ +service = new ApprovalFlowsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/crm/controller/ApprovalInstancesController.php b/app/crm/controller/ApprovalInstancesController.php new file mode 100644 index 0000000..e854af6 --- /dev/null +++ b/app/crm/controller/ApprovalInstancesController.php @@ -0,0 +1,33 @@ +service = new ApprovalInstancesService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + // protected function indexFormat($data) + // { + // if (isset($data['created_at'])) + // $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + // if (isset($data['updated_at'])) + // $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + // return $data; + // } +} \ No newline at end of file diff --git a/app/crm/controller/ApprovalRecordsController.php b/app/crm/controller/ApprovalRecordsController.php new file mode 100644 index 0000000..0c55e0d --- /dev/null +++ b/app/crm/controller/ApprovalRecordsController.php @@ -0,0 +1,87 @@ +service = new ApprovalRecordsService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if (isset($data['records']) && count($data['records']) > 0) { + foreach ($data['records'] as $key => &$record) { + $result = [ + 'id' => $record['id'], + 'flows_name' => $record['approvalInstance']['flows']['name'], + + 'instance_id' => $record['instance_id'], + + 'biz_id' => $record['approvalInstance']['biz_id'], + 'biz_desc' => '备货单【'.$record['approvalInstance']['bizInfo']['order_no'].'】', + 'biz_type' => $record['approvalInstance']['flow_type'], + + 'initiator_name' => $record['approvalInstance']['initiator']['name'], + 'initiator_avatar' => $record['approvalInstance']['initiator']['avatar'], + + 'approver_name' => $record['approver']['name'], + 'approver_avatar' => $record['approver']['avatar'], + + 'step_index' => $record['step_index'], + 'action' => $record['action'], + 'status' => SysDictItem::where('item_value', $record['status'])->where('status', 1)->value('item_label'), + 'comment' => $record['comment'], + 'approved_at' => $record['approved_at'], + + 'finished_at' => $record['approvalInstance']['finished_at'], + 'deadline_at' => $record['approvalInstance']['deadline_at'], + + 'created_at' => $record['created_at'] + ]; + $record = $result; + } + } + return $data; + } + + + public function approve(Request $request) + { + try { + $res = $this->service->serviceApprove($request); + return $this->success([], $res['msg']); + } catch (\Throwable $e) { + 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/crm/controller/StockOrdersController.php b/app/crm/controller/StockOrdersController.php index 0930a1a..cd2b382 100644 --- a/app/crm/controller/StockOrdersController.php +++ b/app/crm/controller/StockOrdersController.php @@ -34,6 +34,19 @@ class StockOrdersController extends BaseController if (isset($data['eta_ship_date'])) $data['eta_ship_date'] = date('Y-m-d', $data['eta_ship_date']); if (isset($data['biz_date'])) $data['biz_date'] = date('Y-m-d', $data['biz_date']); if (isset($data['latest_arrival_date'])) $data['latest_arrival_date'] = date('Y-m-d', $data['latest_arrival_date']); + + if (isset($data['id'])) { + $appro = new \app\common\utils\approval\service\ApprovalFlowService(); + $appro = $appro->getBriefStatusByBiz($data['id'], null, 'name'); + $map = ['processing'=>'审批中','pending'=>'审批中','approved'=>'已通过','rejected'=>'已拒绝','revoked'=>'已撤回']; + if ($appro) { + $data['appro_status'] = [ + 'status' => $appro['status'], + 'status_desc' => $map[$appro['status']], + 'name' => isset($appro['step'])?$appro['step']['name']:'' + ]; + } + } return $data; } @@ -107,13 +120,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/crm/logic/ApprovalFlowsLogic.php b/app/crm/logic/ApprovalFlowsLogic.php new file mode 100644 index 0000000..8a5260f --- /dev/null +++ b/app/crm/logic/ApprovalFlowsLogic.php @@ -0,0 +1,17 @@ + 'array', + ]; +} \ No newline at end of file diff --git a/app/crm/model/ApprovalInstances.php b/app/crm/model/ApprovalInstances.php new file mode 100644 index 0000000..b5a9a7b --- /dev/null +++ b/app/crm/model/ApprovalInstances.php @@ -0,0 +1,107 @@ + 'array', // 自动转换 JSON 字段为数组 + 'finished_at' => 'datetime', + 'deadline_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 一个审批实例可以有多个审批记录 + */ + public function approvalRecords(): HasMany + { + return $this->hasMany(ApprovalRecords::class, 'instance_id'); + } + + /** + * 获取当前步骤的所有审批记录(pending 状态) + */ + public function currentStepRecords() + { + return $this->approvalRecords() + ->where('step_index', $this->current_step) + ->where('status', 'pending'); + } + + public function flows() + { + return $this->belongsTo(ApprovalFlows::class, 'flow_type', 'type'); + } + + public function initiator() + { + return $this->belongsTo(User::class, 'initiator_id', 'id'); + } + + + public function bizInfo() + { + return $this->hasOne(StockOrders::class, 'id', 'biz_id'); + } +} \ No newline at end of file diff --git a/app/crm/model/ApprovalRecords.php b/app/crm/model/ApprovalRecords.php new file mode 100644 index 0000000..4f4ea06 --- /dev/null +++ b/app/crm/model/ApprovalRecords.php @@ -0,0 +1,84 @@ + 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 一条审批记录属于一个审批实例 + */ + public function approvalInstance(): BelongsTo + { + return $this->belongsTo(ApprovalInstances::class, 'instance_id'); + } + + /** + * 关联审批人 + */ + public function approver() + { + return $this->belongsTo(User::class, 'approver_id'); + } +} \ No newline at end of file diff --git a/app/crm/service/ApprovalFlowsService.php b/app/crm/service/ApprovalFlowsService.php new file mode 100644 index 0000000..e373c07 --- /dev/null +++ b/app/crm/service/ApprovalFlowsService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/crm/service/ApprovalInstancesService.php b/app/crm/service/ApprovalInstancesService.php new file mode 100644 index 0000000..86e6e57 --- /dev/null +++ b/app/crm/service/ApprovalInstancesService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/crm/service/ApprovalRecordsService.php b/app/crm/service/ApprovalRecordsService.php new file mode 100644 index 0000000..16c070d --- /dev/null +++ b/app/crm/service/ApprovalRecordsService.php @@ -0,0 +1,91 @@ +validator; + } + + + protected function buildQueryConfig(Request $request, string $scene = 'default'): array + { + $p = $request->all(); + + $config = []; + $config['model'] = $this->modelClass; + switch ($scene) { + case 'index': + $config['order']['id'] = 'desc'; + + $config['with'] = [ + 'approvalInstance', + 'approver', + 'approvalInstance.flows', + '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']; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + case 'logs': + $config['where']['instance_id'] = $p['instance_id']; + $config['order']['id'] = 'desc'; + break; + } + + return $config; + } + + + public function serviceApprove(Request $request) + { + $param = $request->all(); + $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/crm/validate/ApprovalFlowsValidate.php b/app/crm/validate/ApprovalFlowsValidate.php new file mode 100644 index 0000000..fef36f4 --- /dev/null +++ b/app/crm/validate/ApprovalFlowsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/crm/validate/ApprovalInstancesValidate.php b/app/crm/validate/ApprovalInstancesValidate.php new file mode 100644 index 0000000..bbcbad5 --- /dev/null +++ b/app/crm/validate/ApprovalInstancesValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/crm/validate/ApprovalRecordsValidate.php b/app/crm/validate/ApprovalRecordsValidate.php new file mode 100644 index 0000000..6a91cb6 --- /dev/null +++ b/app/crm/validate/ApprovalRecordsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file 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..7616ca1 100644 --- a/config/route.php +++ b/config/route.php @@ -302,10 +302,15 @@ 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('/action', [app\admin\controller\ApprovalRecordsController::class, 'approve']); - Route::any('/destroy', [app\admin\controller\ApprovalRecordsController::class, 'destroy']); + }); + + Route::group('/stock/order', function () { + Route::any('/list', [app\admin\controller\StockOrdersController::class, 'index']); + Route::any('/show', [app\admin\controller\StockOrdersController::class, 'show']); + Route::any('/store', [app\admin\controller\StockOrdersController::class, 'store']); + Route::any('/print', [app\admin\controller\StockOrdersController::class, 'print']); }); }); @@ -377,12 +382,12 @@ Route::group('/crm', function () { Route::any('/show', [app\crm\controller\StockOrdersController::class, 'show']); Route::any('/store', [app\crm\controller\StockOrdersController::class, 'store']); Route::any('/print', [app\crm\controller\StockOrdersController::class, 'print']); - // Route::any('/update', [app\crm\controller\StockOrdersController::class, 'update']); - // Route::any('/destroy', [app\crm\controller\StockOrdersController::class, 'destroy']); }); - // Route::group('/pdf', function () { - // Route::any('/bx/{id}', [app\common\utils\pdf\controller\DocumentController::class, 'downloadReimbursementPdf']); - // Route::any('/ckbx/{id}', [app\common\utils\pdf\controller\DocumentController::class, 'viewReimbursementPdf']); - // }); + // 审批记录 + Route::group('/approval/records', function () { + Route::any('/list', [app\crm\controller\ApprovalRecordsController::class, 'index']); + Route::any('/logs', [app\crm\controller\ApprovalRecordsController::class, 'logs']); + Route::any('/action', [app\crm\controller\ApprovalRecordsController::class, 'approve']); + }); }); \ No newline at end of file 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