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
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace app\flow\strategies;
use app\flow\ProcessContext;
use app\flow\nodes\ProcessNodeInterface;
/**
* 策略抽象基类
*/
abstract class AbstractStrategy implements ProcessStrategyInterface
{
/**
* 策略配置
*/
protected array $config = [];
/**
* 策略执行阶段
*/
protected string $phase = 'before';
public function __construct(array $config = [])
{
$this->config = array_merge($this->config, $config);
}
/**
* 执行策略
*/
public function execute(ProcessContext $context, ProcessNodeInterface $node): ProcessContext
{
if (!$this->isApplicable($context, $node)) {
return $context;
}
return $this->doExecute($context, $node);
}
/**
* 具体执行逻辑,由子类实现
*/
abstract protected function doExecute(ProcessContext $context, ProcessNodeInterface $node): ProcessContext;
/**
* 获取策略执行阶段
*/
public function getPhase(): string
{
return $this->phase;
}
/**
* 设置执行阶段
*/
public function setPhase(string $phase): self
{
$this->phase = $phase;
return $this;
}
/**
* 判断策略是否适用
* 默认总是适用
*/
public function isApplicable(ProcessContext $context, ProcessNodeInterface $node): bool
{
return true;
}
/**
* 获取配置
*/
protected function getConfig(string $key, mixed $default = null): mixed
{
return $this->config[$key] ?? $default;
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
namespace app\flow\strategies;
use app\flow\config\MorningMode;
use app\flow\config\MorningWashConfig;
use app\flow\ProcessContext;
use app\flow\nodes\ProcessNodeInterface;
use app\utils\Logger;
/**
* 晨洗判断策略
* 根据配置判断是否需要晨洗
*/
class MorningWashStrategy extends AbstractStrategy
{
protected string $phase = 'before';
protected MorningWashConfig $morningWashConfig;
public function __construct(MorningWashConfig $config)
{
parent::__construct([]);
$this->morningWashConfig = $config;
Logger::debug("MorningWashStrategy 初始化完成");
}
/**
* 执行晨洗判断
*/
protected function doExecute(ProcessContext $context, ProcessNodeInterface $node): ProcessContext
{
$isEnd = $context->currentStep == '结束';
$needMorning = $this->checkNeedMorningWash($context) && $isEnd;
$context->needMorningWash = $needMorning;
// 如果不需要晨洗,标记为已完成
if (!$needMorning) {
$context->morningWashed = true;
}
return $context;
}
/**
* 检查是否需要晨洗
*/
protected function checkNeedMorningWash(ProcessContext $context): bool
{
return match ($this->morningWashConfig->mode) {
MorningMode::None => false,
MorningMode::All => true,
MorningMode::StorageTime => $this->checkByStorageTime($context),
MorningMode::DailyFirst => $this->checkByDailyFirst($context),
MorningMode::SpecificTypes => $this->checkBySpecificTypes($context),
};
}
/**
* 根据存储时间判断(义乌模式)
* 普通镜柜超过阈值小时需要晨洗,无菌镜柜免晨消
*/
protected function checkByStorageTime(ProcessContext $context): bool
{
$storageInTime = $context->storageInTime;
$lastActionType = $context->previousAction->action_type_name;
$lastProcessName = $context->previousAction->process_name;
// 如果最后一次操作是存储且已取出
if ($lastActionType === '存储' && $lastProcessName === '内镜取出') {
return false;
}
// 如果最后一次操作是存储且未取出,检查存储时间
if ($lastActionType === '存储' && $lastProcessName === '内镜放入' && $storageInTime) {
$storageHours = (time() - strtotime($storageInTime)) / 3600;
return $storageHours > $this->morningWashConfig->storageThreshold;
}
// 检查今天是否已有洗消记录
return $this->hasWashRecordToday($context);
}
/**
* 根据每天第一次判断(忠县模式)
*/
protected function checkByDailyFirst(ProcessContext $context): bool
{
return $this->hasWashRecordToday($context);
}
/**
* 检查今天是否已有洗消记录
*/
protected function hasWashRecordToday(ProcessContext $context): bool
{
return $context->todayWashRecords === 0;
}
/**
* 根据特定类型判断
*/
protected function checkBySpecificTypes(ProcessContext $context): bool
{
$specificTypes = $this->morningWashConfig->getExpand('specific_types', []);
return in_array($context->endoscopeType, $specificTypes);
}
/**
* 获取策略名称
*/
public function getName(): string
{
return '晨洗判断策略(' . $this->morningWashConfig->mode->name . ')';
}
}
@@ -0,0 +1,41 @@
<?php
namespace app\flow\strategies;
use app\flow\ProcessContext;
use app\flow\nodes\ProcessNodeInterface;
/**
* 流程策略接口
* 策略模式的核心接口
*/
interface ProcessStrategyInterface
{
/**
* 执行策略
* @param ProcessContext $context 流程上下文
* @param ProcessNodeInterface $node 当前节点
* @return ProcessContext 处理后的上下文
*/
public function execute(ProcessContext $context, ProcessNodeInterface $node): ProcessContext;
/**
* 获取策略名称
* @return string
*/
public function getName(): string;
/**
* 获取策略执行阶段
* @return string 'before' | 'after'
*/
public function getPhase(): string;
/**
* 判断策略是否适用
* @param ProcessContext $context
* @param ProcessNodeInterface $node
* @return bool
*/
public function isApplicable(ProcessContext $context, ProcessNodeInterface $node): bool;
}
@@ -0,0 +1,143 @@
<?php
namespace app\flow\strategies;
use app\config\Config;
use app\flow\config\TimeValidationConfig;
use app\flow\ProcessContext;
use app\flow\nodes\ProcessNodeInterface;
use app\flow\VoiceMessage;
use app\repository\ProcessDurationRepository;
use app\utils\Logger;
/**
* 时间验证策略
* 验证步骤时间是否满足要求
*/
class TimeValidationStrategy extends AbstractStrategy
{
protected string $phase = 'before';
protected TimeValidationConfig $timeValidationConfig;
/**
* 已从数据库加载的时长缓存:[processType => [stepName => seconds]]
*/
private array $dbDurationCache = [];
public function __construct(TimeValidationConfig $config)
{
parent::__construct([]);
$this->timeValidationConfig = $config;
Logger::debug("TimeValidationStrategy 初始化完成");
}
/**
* 从数据库获取步骤时长(按流程类型精确匹配)
* 优先级:数据库记录 > 构造时配置 > 内置 fallback
*/
protected function getDurationFromDb(string $stepCode, string $processType): int
{
if (empty($processType)) {
return $this->timeValidationConfig->getDuration($stepCode, $processType);
}
// 整批加载并缓存,减少查询次数
if (!isset($this->dbDurationCache[$processType])) {
$this->dbDurationCache[$processType] = ProcessDurationRepository::new()
->getDurationsByProcessType($processType);
}
$dbValue = $this->dbDurationCache[$processType][$stepCode] ?? null;
if ($dbValue !== null) {
return $dbValue;
}
// fallback:返回配置中的默认值
return $this->timeValidationConfig->getDuration($stepCode);
}
/**
* 执行时间验证
*/
protected function doExecute(ProcessContext $context, ProcessNodeInterface $node): ProcessContext
{
$stepCode = $node->getCode();
$currentStep = $context->currentStep;
$processType = $context->processType;
Logger::debug("开始执行时间验证策略,步骤:{$stepCode},流程类型:{$processType}");
$configDuration = $this->timeValidationConfig->getDuration($stepCode,$processType);
Logger::debug("步骤:{$stepCode},流程类型:{$processType},配置时长:{$configDuration}s");
if ($configDuration > 0) {
// 配置中有明确时长,直接使用
$requiredDuration = $configDuration;
} else {
Logger::debug("步骤:{$stepCode},流程类型:{$processType},无配置时长,从数据库查询");
// 从数据库按流程类型精确查询
$requiredDuration = $this->getDurationFromDb($stepCode, $context->processType);
if ($requiredDuration > 0) {
$context->setStepDuration($stepCode, $requiredDuration);
} else {
// 最后使用上下文已缓存值
$requiredDuration = $context->getStepDuration($stepCode);
}
}
// 无需时间验证,直接放行
if ($requiredDuration <= 0) {
return $context;
}
// 获取上次该步骤的操作时间
$duration = $context->duration;
// 没有上次记录(第一次操作),允许通过
if (empty($duration)) {
Logger::debug("步骤:{$stepCode},流程类型:{$processType},无上次记录,允许通过");
return $context;
}
if ($duration < $requiredDuration) {
Logger::debug("[{$stepCode}] 时间不足,剩余: {}s", [$requiredDuration - $duration]);
if (Config::getInstance()->blockMode){
$voice = VoiceMessage::NOT_ENOUGH_TIME->value;
$voice = str_replace('{step}', $stepCode, $voice);
$voice = str_replace('{time}', $requiredDuration - $duration, $voice);
$context->setCustomError($voice);
}
return $context;
}
return $context;
}
/**
* 判断策略是否适用
* 只有在 timeValidationConfig 中登记的步骤才参与时间验证
*/
public function isApplicable(ProcessContext $context, ProcessNodeInterface $node): bool
{
if ($context->currentStep != $context->previousAction->process_name) return false;
if (!$this->timeValidationConfig->hasStep($node->getCode(), $context->processType)) return false;
return true;
}
/**
* 获取策略名称
*/
public function getName(): string
{
return '时间验证策略';
}
/**
* 手动设置步骤时长(覆盖 fallback,不影响数据库缓存)
*/
public function setStepDuration(string $stepCode, int $seconds): self
{
$this->timeValidationConfig->setDuration($stepCode, $seconds);
return $this;
}
}
@@ -0,0 +1,173 @@
<?php
namespace app\flow\strategies;
use app\flow\config\VoiceTemplatesConfig;
use app\flow\exception\IllegalUsageException;
use app\flow\ProcessContext;
use app\flow\nodes\ProcessNodeInterface;
use app\utils\Logger;
/**
* 语音生成策略
* 根据配置生成语音播报内容
*/
class VoiceGenerationStrategy extends AbstractStrategy
{
protected string $phase = 'after';
protected VoiceTemplatesConfig $voiceTemplatesConfig;
public function __construct(VoiceTemplatesConfig $config)
{
parent::__construct([]);
$this->voiceTemplatesConfig = $config;
Logger::debug("VoiceGenerationStrategy 初始化完成");
}
/**
* 执行语音生成
*/
protected function doExecute(ProcessContext $context, ProcessNodeInterface $node): ProcessContext
{
// 如果存在流程中自定义语音,就直接输出
if (!empty($context->voiceMessage)) {
Logger::warn(
"流程中存在自定义语音或存在多次设置语音,语音应该在 VoiceGenerationStrategy 策略中配置,否则不能拦截与自定义配置",
new IllegalUsageException("In the existing process, there is custom voice, which should be configured in the VoiceGenerationStrategy strategy; otherwise, it cannot be intercepted and customized")
);
$context->setVoice($context->voiceMessage);
return $context;
}
// 如果已经有错误,生成错误语音
// 否则生成正常流程语音
$voice = !$context->success ? $this->generateErrorVoice($context) : $this->generateNormalVoice($context, $node);
// 应用语音模板
foreach ($context->voiceTemplateParams as $key => $val) {
$replaceVal = match (true) {
is_array($val) => implode(',', $val),
is_bool($val) => $val ? 'true' : 'false',
is_null($val) => '',
default => (string) $val
};
$voice = str_replace('{' . $key . '}', $replaceVal, $voice);
}
Logger::debug("应用语音模板后的内容: {$voice}");
$context->setVoice($voice);
return $context;
}
/**
* 生成正常流程语音
*/
protected function generateNormalVoice(ProcessContext $context, ProcessNodeInterface $node): string
{
$stepCode = $node->getCode();
$stepName = $node->getName();
$processType = $context->processType;
// 根据流程类型选择模板
$templateKey = $this->getTemplateKey($processType, $stepCode, $context);
$templates = $this->voiceTemplatesConfig->getTemplates($templateKey);
if (empty($templates)) {
$templates = $this->voiceTemplatesConfig->normalWash;
}
// 优先使用自定义步骤语音(直接存储在对应模板key中)
$voice = $templates[$stepCode] ?? $stepCode . '完成';
// 添加提醒信息
$remind = $this->getRemindMessage($context);
if ($remind) {
$voice .= $remind;
}
return $voice;
}
/**
* 生成错误语音
*/
protected function generateErrorVoice(ProcessContext $context): string
{
$errorMessage = $context->errorMessage;
$errorTemplates = $this->voiceTemplatesConfig->voiceMessage;
$errorMsg = $errorTemplates[$errorMessage->name] ?? $errorMessage->value;
if (empty($errorMsg)) {
$errorMsg = $context->voiceMessage;
Logger::debug("错误信息配置未命中,使用自定义语音: {$errorMsg}");
} else {
Logger::debug("错误信息配置命中: {$errorMsg}");
}
if (empty($errorMsg)) {
Logger::error("配置文件与枚举信息中未找匹配到错误信息");
}
return $errorMsg ?: '刷卡错误';
}
/**
* 流程类型到模板键的映射
*/
protected const array PROCESS_TYPE_MAP = [
'机洗' => 'machine_wash',
'机洗(晨洗)' => 'machine_wash',
'机洗(加强)' => 'machine_wash',
'测漏' => 'leak_test',
'存储' => 'storage',
];
/**
* 获取模板键
*/
protected function getTemplateKey(string $processType, string $stepCode, ProcessContext $context): string
{
// 晨洗流程中的清洗/机洗步骤
if ($context->needMorningWash) {
return 'morning_wash';
}
// 从映射表中获取,默认返回 normal_wash
return self::PROCESS_TYPE_MAP[$processType] ?? 'normal_wash';
}
/**
* 获取提醒信息
*/
protected function getRemindMessage(ProcessContext $context): string
{
$reminds = [];
if ($context->needLeakTestRemind) {
$reminds[] = ',清洗开始前,请测漏';
}
if ($context->needStorageRemind) {
$reminds[] = ',未登记取出';
}
return implode('', $reminds);
}
/**
* 判断策略是否适用
*/
public function isApplicable(ProcessContext $context, ProcessNodeInterface $node): bool
{
return true;
}
/**
* 获取策略名称
*/
public function getName(): string
{
return '语音生成策略';
}
}