previousAction; } } /** * 流程类型: 手工洗 / 机洗 / 测漏 / 存储 / 手工洗(晨洗)/ 机洗(晨洗) */ public string $processType = ''; /** * 批次号 */ public string $batchNo = ''; /** * 操作开始时间 */ public string $actionStartTime = ''; /** * @var int|null 操作时长(秒) * 除非处理中有节点主动设置,否则一直是 null */ public ?int $duration = null; // ==================== 晨洗相关 ==================== /** * 是否需要晨洗 */ public bool $needMorningWash = false; /** * 是否已完成晨洗 */ public bool $morningWashed = false; /** * 存储入库时间(用于晨洗判断) */ public ?string $storageInTime = null; /** * 今天洗消记录数 */ public int $todayWashRecords = 0; // ==================== 存储相关 ==================== /** * 内镜是否在存储柜中 */ public bool $isInStorage = false; /** * 最后一次存储操作类型:入库/出库 */ public string $lastStorageAction = ''; /** * 晨洗开始时间 */ public string $morningStartTime = '00:00:00'; // ==================== 步骤时间记录 ==================== /** * 各步骤时长要求(从数据库获取)[步骤编码 => 秒数] */ public array $stepDurations = []; // ==================== 操作员信息 ==================== /** * 操作员ID(人员卡) */ public string $operatorId = ''; /** * 操作员姓名 */ public string $operatorName = ''; /** * 操作员RFID */ public string $operatorRfid = ''; // ==================== 提醒标记 ==================== /** * 是否需要测漏提醒 */ public bool $needLeakTestRemind = false; /** * 是否需要存储提醒 */ public bool $needStorageRemind = false; /** * 是否已测漏 */ public bool $leakTestDone = false; /** * 测漏结果 */ public string $leakTestResult = ''; // '正常' / '异常' / '' // ==================== 语音输出 ==================== /** * 语音播报内容 */ public string $voiceMessage = ''; // ==================== 执行结果 ==================== /** * 流程执行结果 */ public bool $success = true { get { return $this->success; } } /** * 错误信息 */ public VoiceMessage $errorMessage = VoiceMessage::NONE; // ==================== 原始数据 ==================== /** * 原始刷卡数据(PacketContext) */ public ?PacketContext $packetContext = null; /** * 原始刷卡数据数组(兼容旧代码) */ public array $rawData = []; // ==================== 数据库相关标记 ==================== /** * 是否需要操作数据库 */ public bool $needDatabaseOperation = false; /** * 数据库操作类型 insert / update * DbOperationType */ public array $dbOperation = [] { get { return $this->dbOperation; } set(DbOperationType|array $value) { if (is_array($value)) { $this->dbOperation = $value; } else { $this->dbOperation[] = $value; } } } /** * 是否需要发送WebSocket通知 */ public bool $needWebSocketNotify = false; /** * 这张卡是否是人员卡(而非内镜卡) * true 时 FlowProcessor 不走流程链,直接存储操作员信息 */ public bool $isOperatorCard = false; public ?ProcessConfig $engineConfig; /** * 期望下一步刷卡提示 * 当节点识别到读卡器类型匹配但流程步骤不符时,由节点自行写入此字段 * ProcessEngine 无节点命中时直接读取此字段作为错误语音提示 */ public VoiceMessage $expectedNextStep = VoiceMessage::NONE { get { return $this->expectedNextStep; } set { // 获取调用这个方法的类名 $callerClass = debug_backtrace()[1]['class'] ?? ''; $callerClass = array_reverse(explode('\\', $callerClass)) [0]; Logger::debug("[{}] 设置节点预期: {}", [$callerClass, $value]); $this->expectedNextStep = $value; } } /** * @var int 设置后从当前节点跳过 skipNodeCount 个节点 */ public int $skipNodeCount = 0; public function __construct() { $this->config = Config::getInstance(); } // ==================== 工具方法 ==================== /** * 创建上下文实例 */ public static function create(array $data = []): self { $context = new self(); foreach ($data as $key => $value) { if (property_exists($context, $key) && $value !== null) { $context->$key = $value; } } return $context; } /** * 从 PacketContext 创建流程上下文,并自动从数据库补全内镜、读卡器及历史记录信息 * * @param PacketContext $packetContext 数据包上下文 * @param array $additionalData 额外补充/覆盖的数据 * @return self */ public static function fromPacketContext(PacketContext $packetContext, array $additionalData = []): self { $context = new self(); $context->packetContext = $packetContext; $context->engineConfig = $additionalData['engineConfig'] ?? null; if ($context->engineConfig !== null) { $context->morningStartTime = $context->engineConfig->getMorningWashConfig()->morningStartTime; } // 1. 初始化基础信息 $context->cardNo = $packetContext->packet->card ?? ''; $context->readerNo = $packetContext->packet->reader ?? ''; $context->rawData = $packetContext->packet->toArray(); // 2. 加载内镜/操作员信息 $context->loadEndoscopeOrOperatorInfo(); // 3. 加载读卡器信息 $context->loadReaderInfo(); // 4. 加载内镜操作记录相关信息(仅当内镜ID存在时执行) if (!empty($context->endoscopeId)) { $context->loadEndoscopeActionInfo(); $context->loadStorageStatus(); } // 5. 合并额外数据(外部可覆盖以上任意字段) foreach ($additionalData as $key => $value) if (property_exists($context, $key)) { $context->$key = $value; } Logger::debug("从 PacketContext 创建 ProcessContext 流程上下文"); return $context; } /** * 加载内镜或操作员信息 */ private function loadEndoscopeOrOperatorInfo(): void { if (empty($this->cardNo)) { return; } // 优先查询内镜信息 $endoscope = EndoscopeRepository::new()->findByCardNo($this->cardNo); if ($endoscope !== null) { $this->endoscopeId = (string)$endoscope->endoscope_id; $this->endoscopeName = (string)$endoscope->endoscope_name; $this->endoscopeType = (string)$endoscope->endoscope_type; return; } // 内镜无记录则查询人员卡信息 try { $user = UserRepository::new()->findByRfid($this->cardNo); if ($user !== null) { $this->isOperatorCard = true; $this->operatorId = (string)$user->user_id; $this->operatorName = (string)$user->user_name; $this->operatorRfid = (string)$user->user_rfid; } } catch (Exception $e) { Logger::error("[ProcessContext] 查询人员卡信息出错: {}", [$e->getMessage()]); } } /** * 加载读卡器信息 */ private function loadReaderInfo(): void { if (empty($this->readerNo)) { return; } $readerInfo = ReaderRepository::new()->findReaderInfo($this->readerNo); if ($readerInfo !== null) { $this->readerId = $readerInfo['readerId']; $this->readerType = $readerInfo['readerType']; } } /** * 加载内镜操作记录相关信息 */ private function loadEndoscopeActionInfo(): void { $actionsRepo = EctActionsRepository::new(); // 查询最后一条操作记录 $lastAction = $actionsRepo->findLastAction($this->endoscopeId, [0, 7, 8]); if (empty($lastAction)) { return; } $lastStepStartTime = $lastAction->op_starttime; $this->previousAction = $lastAction; $this->duration = time() - strtotime($lastStepStartTime); // 计算操作时长(秒) $this->handleLastAction($lastAction, $actionsRepo); // 查询今日洗消记录数(晨洗判断) $this->todayWashRecords = $actionsRepo->countTodayActions($this->endoscopeId, $this->morningStartTime, [0, 7, 8]); } /** * 加载内镜存储状态 */ private function loadStorageStatus(): void { $actionsRepo = EctActionsRepository::new(); // 查询最后一次存储操作记录 $lastStorageAction = $actionsRepo->findLastStorageAction($this->endoscopeId); if ($lastStorageAction !== null) { $this->isInStorage = ($lastStorageAction['process_name'] === '内镜放入'); $this->lastStorageAction = $lastStorageAction['process_name']; if ($this->isInStorage) { $this->storageInTime = $lastStorageAction['op_starttime']; } } // 查询最后一次存储入库时间(义乌模式晨洗判断) $storageTime = $actionsRepo->findLastStorageTime($this->endoscopeId); if ($storageTime !== null) { $this->storageInTime = $storageTime; } } /** * 处理最后一条操作记录的核心逻辑 * * @param EctActions $lastAction 最后一条操作记录 * @param EctActionsRepository $actionsRepo 操作记录仓储 */ private function handleLastAction(EctActions $lastAction, EctActionsRepository $actionsRepo): void { // 设置基础操作信息 $this->currentStep = (string)$lastAction->process_name; $this->actionStartTime = date('Y-m-d H:i:s'); // 处理批次号逻辑 // 结束步骤:创建新批次;非结束步骤:使用历史批次并处理旧批次 if ($this->currentStep === '结束') { $newBatchNo = $this->getOrCreateBatchNo(true); $this->batchNo = $newBatchNo; } else { $this->batchNo = (string)$lastAction->op_batchno; $this->processType = (string)$lastAction->action_type_name; } // 加载批次操作员信息 if ($this->currentStep !== '结束' && !empty($this->batchNo)) { $this->loadBatchOperatorInfo($actionsRepo); } } /** * 加载批次对应的操作员信息 * * @param EctActionsRepository $actionsRepo 操作记录仓储 */ private function loadBatchOperatorInfo(EctActionsRepository $actionsRepo): void { $operator = $actionsRepo->findOperatorByBatchNo($this->batchNo); if ($operator !== null) { $this->operatorId = $operator['id']; $this->operatorName = $operator['name']; $this->operatorRfid = $operator['rfid']; } } // ==================== 非静态方法 ==================== /** * 设置错误状态。 * 注:同时会将 success 字段设置为 false * 注:设置后会阻断自动流程的 next 步骤执行 */ public function setError(VoiceMessage $message): self { $this->errorMessage = $message; $this->success = false; return $this; } /** * 设置错误状态。 * 注:同时会将 success 字段设置为 false * 注:设置后会阻断自动流程的 next 步骤执行 * @param string $message * @return $this */ public function setCustomError(string $message): self { $this->errorMessage = VoiceMessage::CUSTOM; $this->voiceMessage = $message; $this->success = false; return $this; } /** * 设置语音消息 */ public function setVoice(string|VoiceMessage $message): self { $illegalCaller = false; $targetIndex = 2; // 目标层级索引 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6); foreach ($backtrace as $index => $trace) { if ($index === $targetIndex) { $caller = $trace['class'] ?? ''; if (!empty($caller) && $caller === 'VoiceGenerationStrategy') $illegalCaller = true; break; } } if ($illegalCaller) Logger::warn( "不应该在 VoiceGenerationStrategy 之外的地方调用 setVoice 方法", new IllegalUsageException("Not allowed to call setVoice method outside of VoiceGenerationStrategy") ); if ($message instanceof VoiceMessage) { $this->voiceMessage = $message->value; } else { $this->voiceMessage = $message; } return $this; } /** * 添加语音前缀(内镜名称等) */ public function prependVoice(string $prefix): self { $this->voiceMessage = $prefix . $this->voiceMessage; return $this; } /** * 获取完整语音(包含内镜名称) */ public function getFullVoice(): string { // $name = str_replace(['北院', '南院', '电子'], '', $this->endoscopeName); // return $this->endoscopeName . $this->voiceMessage; $message = $this->voiceMessage; if (empty($message)) $message = $this->errorMessage->value; return $message; } // ==================== 批次号管理(分布式一致性) ==================== /** * 获取或创建批次号 * * 分布式场景下的批次号一致性保证: * 1. 首先尝试从数据库获取该内镜当前未完成的流程批次号 * 2. 如果没有未完成的流程,生成新的批次号 * 3. 批次号格式:年月日时分秒 + 内镜ID + 随机数 * * @param bool $forceNew 强制生成新批次号(用于开始新流程) * @return string 批次号 */ public function getOrCreateBatchNo(bool $forceNew = false): string { // 如果已有批次号且不强制新建,直接返回 if (!$forceNew && !empty($this->batchNo)) { return $this->batchNo; } // 从数据库查询最大的批次号 $existingBatchNo = null; if (!empty($this->endoscopeId)) $existingBatchNo = EctActionsRepository::new()->findTodayActiveBatchNo($this->config->machineId); return $this->generateBatchNo($existingBatchNo); } /** * 生成批次号 * * 批次号格式:YYYYMMDD + 机器ID(两位) + 顺序递增(4位) * * @param string|null $existingBatchNo 已存在的最大批次号(无则传null) * @return string 批次号 */ public function generateBatchNo(?string $existingBatchNo = null): string { // 1. 生成日期部分(仅YYYYMMDD,符合需求格式) $datePart = date('Ymd'); // 2. 初始化序号为1(默认初始值) $sequence = 1; // 3. 如果存在已有批次号,解析并递增序号 if (!empty($existingBatchNo)) { // 截取批次号的日期部分(前8位) $existingDatePart = substr($existingBatchNo, 0, 8); // 截取批次号的序号部分(后4位) $existingSequence = substr($existingBatchNo, 10, 4); // 如果已有批次号的日期和今天一致,序号递增 if ($existingDatePart === $datePart && is_numeric($existingSequence)) { $sequence = (int)$existingSequence + 1; } // 如果日期不一致,序号重置为1(无需额外处理,默认就是1) } // 4. 将序号补零为4位(如1→0001,10→0010,100→0100) $sequencePart = str_pad($sequence, 4, '0', STR_PAD_LEFT); // 5. 拼接并返回最终批次号 return $datePart . $this->config->machineId . $sequencePart; } /** * 设置批次号 */ public function setBatchNo(string $batchNo): self { $this->batchNo = $batchNo; return $this; } /** * @param string $batchNo * @return array */ public static function parseBatchNo(string $batchNo): array { // 批次号格式:YYYYMMDD + 机器ID(两位) + 顺序递增(4位) $datePart = substr($batchNo, 0, 8); $machineId = substr($batchNo, 8, 2); $sequencePart = substr($batchNo, 10, 4); return [ 'date' => $datePart, 'machineId' => $machineId, 'sequence' => $sequencePart, ]; } // ==================== 步骤时间操作方法 ==================== /** * 获取步骤时长要求(秒) * 优先级:上下文已缓存 > 数据库(按 processType 精确匹配)> 内置 fallback */ public function getStepDuration(string $stepCode): int { // 优先使用上下文中已缓存的时长 if (isset($this->stepDurations[$stepCode])) { return $this->stepDurations[$stepCode]; } // 从数据库按流程类型精确查询 if (!empty($this->processType)) { $dbValue = ProcessDurationRepository::new() ->getDurationByProcessTypeAndName($this->processType, $stepCode); if ($dbValue !== null) { // 写入缓存,避免后续重复查询 $this->stepDurations[$stepCode] = $dbValue; return $dbValue; } } // 内置 fallback(与 ect_meta_process 表数据对齐) $defaults = [ '清洗' => 120, // 手工洗/机洗最短 60~120s,取保守值 '漂洗' => 60, // 60s '消毒' => 300, // 300s '终末漂洗' => 120, // 120s '干燥' => 30, // 30s '机洗' => 360, // 机洗(晨洗)360s ]; return $defaults[$stepCode] ?? 0; } /** * 设置步骤时长要求 */ public function setStepDuration(string $stepCode, int $seconds): self { $this->stepDurations[$stepCode] = $seconds; return $this; } // ==================== 流程状态检查 ==================== /** * 检查是否可以开始新流程 */ public function canStartNewProcess(): bool { // 当前步骤为空、结束、内镜取出、测漏正常时可以开始新流程 $validSteps = ['', '结束', '内镜取出', '测漏正常', '测漏异常']; return in_array($this->currentStep, $validSteps); } /** * 检查是否已完成清洗流程 */ public function isWashProcessCompleted(): bool { return $this->currentStep === '结束'; } public function hasOperator(): bool { return !empty($this->operatorRfid) && !empty($this->operatorId) && !empty($this->operatorName); } }