Files
tcpserver-flow/app/flow/ProcessContext.php
T
2026-03-08 22:58:56 +08:00

738 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\flow;
use app\config\Config;
use app\flow\config\AbstractConfig;
use app\flow\config\ProcessConfig;
use app\flow\exception\IllegalUsageException;
use app\model\EctActions;
use app\net\PacketContext;
use app\repository\EctActionsRepository;
use app\repository\EndoscopeRepository;
use app\repository\ProcessDurationRepository;
use app\repository\ReaderRepository;
use app\repository\UserRepository;
use app\utils\Logger;
use Exception;
/**
* 流程上下文类
* 用于在责任链节点之间传递数据
*/
class ProcessContext
{
/**
* TODO
* @var bool 是否需要增强洗
*/
public bool $needEnhanceWash = false;
/**
* 语音模板参数
* key-value
* @var array
*/
public array $voiceTemplateParams = [];
/**
* 配置类
* @var Config
*/
private Config $config;
// ==================== 基础信息 ====================
/**
* 内镜ID
*/
public string $endoscopeId = '';
/**
* 内镜名称
*/
public string $endoscopeName = '';
/**
* 内镜RFID卡号
*/
public string $cardNo = '';
/**
* 内镜类型(胃镜/肠镜等)
*/
public string $endoscopeType = '';
// ==================== 读卡器信息 ====================
/**
* 读卡器编号
*/
public string $readerNo = '';
/**
* 当前读卡器类型/功能
*/
public string $readerType = '';
/**
* 读卡器ID
*/
public string $readerId = '';
// ==================== 流程状态 ====================
/**
* 当前操作步骤
*/
public string $currentStep = '';
/**
* @var EctActions 上一个操作步骤
*/
public EctActions $previousAction {
get {
return $this->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→000110→0010100→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);
}
}