feat: 实现TCP Server

This commit is contained in:
zimoyin
2026-03-02 21:59:43 +08:00
parent 043306819b
commit a79dfae57d
144 changed files with 15785 additions and 140 deletions
+737
View File
@@ -0,0 +1,737 @@
<?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);
}
}