Files
tcpserver-flow/app/flow/docs/03-DIY扩展.md
T
2026-03-08 22:58:56 +08:00

15 KiB

内镜清洗流程管理系统 - DIY扩展指南

一、扩展概述

本系统采用责任链模式 + 策略模式设计,提供了良好的扩展性。你可以:

  • 添加新的流程节点
  • 添加新的策略
  • 自定义流程配置
  • 修改现有节点行为

二、添加新的流程节点

2.1 场景示例

假设医院需要一个"预处理"步骤,在清洗之前进行。

2.2 创建节点类

<?php

namespace app\flow\nodes;

use app\flow\ProcessContext;

/**
 * 预处理节点
 * 在正式清洗前进行预处理
 */
class PreprocessNode extends AbstractProcessNode
{
    /**
     * 获取节点名称
     */
    public function getName(): string
    {
        return '预处理';
    }

    /**
     * 获取节点编码
     */
    public function getCode(): string
    {
        return '预处理';
    }

    /**
     * 判断当前节点是否能处理该步骤
     */
    public function canHandle(ProcessContext $context): bool
    {
        // 检查当前读卡器类型
        if ($context->readerType !== '预处理') {
            return false;
        }

        // 上一个步骤必须是空或结束
        $validSteps = ['', '结束', '内镜取出'];
        return in_array($context->currentStep, $validSteps);
    }

    /**
     * 具体处理逻辑
     */
    protected function doHandle(ProcessContext $context): ProcessContext
    {
        // 更新步骤
//        $context->lastStep = $context->currentStep;
        $context->currentStep = '预处理';

        // 记录操作时间
        $context->setStepLastTime('预处理', date('Y-m-d H:i:s'));

        // TODO: 插入数据库记录
        $context->needDatabaseOperation = true;
        $context->dbOperation = 'insert';

        // TODO: 发送WebSocket通知
        $context->needWebSocketNotify = true;

        return $context;
    }
}

2.3 注册节点

use app\flow\ProcessEngine;
use app\flow\nodes\PreprocessNode;

$engine = ProcessEngine::createStandard();

// 创建节点实例
$preprocessNode = new PreprocessNode();

// 添加策略
$preprocessNode->addStrategy($engine->getStrategy('voice_generation'));

// 插入到责任链中(在清洗节点之前)
$washNode = $engine->getNode('清洗');
$preprocessNode->setNext($washNode);

// 更新链头
// 注意:这里需要修改 ProcessEngine 的内部实现

2.4 通过配置添加

use app\flow\config\ProcessConfig;

$config = new ProcessConfig([
    'steps' => [
        ['code' => '晨洗', 'class' => 'MorningWashNode', 'enabled' => true],
        ['code' => '预处理', 'class' => 'PreprocessNode', 'enabled' => true], // 新增
        ['code' => '清洗', 'class' => 'WashNode', 'enabled' => true],
        ['code' => '漂洗', 'class' => 'RinseNode', 'enabled' => true],
        ['code' => '消毒', 'class' => 'DisinfectNode', 'enabled' => true],
        ['code' => '终末漂洗', 'class' => 'FinalRinseNode', 'enabled' => true],
        ['code' => '干燥', 'class' => 'DryNode', 'enabled' => true],
        ['code' => '结束', 'class' => 'EndNode', 'enabled' => true],
    ],
]);

$engine = new ProcessEngine($config);

三、添加新的策略

3.1 场景示例

假设需要添加一个"操作员验证策略",验证是否已刷人员卡。

3.2 创建策略类

<?php

namespace app\flow\strategies;

use app\flow\ProcessContext;
use app\flow\nodes\ProcessNodeInterface;

/**
 * 操作员验证策略
 * 验证是否已刷人员卡
 */
class OperatorValidationStrategy extends AbstractStrategy
{
    protected string $phase = 'before';

    /**
     * 执行策略
     */
    protected function doExecute(ProcessContext $context, ProcessNodeInterface $node): ProcessContext
    {
        // 检查是否已刷人员卡
        if (empty($context->operatorId)) {
            $context->setError('请先刷人员卡');
            return $context;
        }

        // 检查人员卡是否有效
        if (!$this->isValidOperator($context->operatorId)) {
            $context->setError('人员卡无效');
            return $context;
        }

        return $context;
    }

    /**
     * 验证操作员是否有效
     * TODO: 从数据库查询
     */
    protected function isValidOperator(string $operatorId): bool
    {
        // TODO: 查询数据库验证操作员
        // SQL: SELECT COUNT(*) FROM ect_user WHERE user_id = ? AND status = 1
        return true;
    }

    /**
     * 判断策略是否适用
     * 只有清洗步骤需要验证操作员
     */
    public function isApplicable(ProcessContext $context, ProcessNodeInterface $node): bool
    {
        return $node->getCode() === '清洗';
    }

    /**
     * 获取策略名称
     */
    public function getName(): string
    {
        return '操作员验证策略';
    }
}

3.3 注册策略

use app\flow\ProcessEngine;
use app\flow\strategies\OperatorValidationStrategy;

$engine = ProcessEngine::createStandard();

// 创建策略实例
$operatorStrategy = new OperatorValidationStrategy();

// 添加到引擎
$engine->addStrategy('operator_validation', $operatorStrategy);

// 将策略添加到指定节点
$washNode = $engine->getNode('清洗');
$washNode->addStrategy($operatorStrategy);

四、修改现有节点行为

4.1 继承并覆盖

<?php

namespace app\flow\nodes\custom;

use app\flow\nodes\WashNode;
use app\flow\ProcessContext;

/**
 * 自定义清洗节点
 * 覆盖父类的某些行为
 */
class CustomWashNode extends WashNode
{
    /**
     * 覆盖处理逻辑
     */
    protected function doHandle(ProcessContext $context): ProcessContext
    {
        // 调用父类逻辑
        $context = parent::doHandle($context);

        // 添加自定义逻辑
        // 例如:记录额外的日志
        $this->logCustomInfo($context);

        return $context;
    }

    /**
     * 自定义验证逻辑
     */
    public function canHandle(ProcessContext $context): bool
    {
        // 先调用父类验证
        if (!parent::canHandle($context)) {
            return false;
        }

        // 添加额外的验证条件
        // 例如:某些类型的内镜不能在此节点处理
        if ($context->endoscopeType === '特殊镜') {
            return false;
        }

        return true;
    }

    /**
     * 自定义日志
     */
    protected function logCustomInfo(ProcessContext $context): void
    {
        // 自定义日志逻辑
        error_log("[CustomWash] 内镜: {$context->endoscopeName}, 时间: " . date('Y-m-d H:i:s'));
    }
}

4.2 使用配置替换节点

$config = new ProcessConfig([
    'steps' => [
        ['code' => '清洗', 'class' => 'app\flow\nodes\custom\CustomWashNode', 'enabled' => true],
        // ... 其他步骤
    ],
]);

五、自定义流程配置加载器

5.1 从数据库加载配置

<?php

namespace app\flow\config;

/**
 * 数据库配置加载器
 */
class DatabaseConfigLoader
{
    /**
     * 从数据库加载医院配置
     */
    public static function load(string $hospitalId): ProcessConfig
    {
        // TODO: 从数据库查询医院配置
        // SQL: SELECT * FROM ect_hospital_config WHERE hospital_id = ?
        
        $configData = [
            'morning_wash' => [
                'mode' => 'daily_first',
            ],
            'steps' => self::loadStepsFromDb($hospitalId),
            'time_validation' => [
                'durations' => self::loadDurationsFromDb($hospitalId),
            ],
        ];

        return new ProcessConfig($configData);
    }

    /**
     * 从数据库加载步骤配置
     */
    protected static function loadStepsFromDb(string $hospitalId): array
    {
        // TODO: 查询数据库
        // SQL: SELECT * FROM ect_hospital_steps WHERE hospital_id = ? ORDER BY step_order
        
        return [
            ['code' => '清洗', 'class' => 'WashNode', 'enabled' => true],
            ['code' => '消毒', 'class' => 'DisinfectNode', 'enabled' => true],
            ['code' => '结束', 'class' => 'EndNode', 'enabled' => true],
        ];
    }

    /**
     * 从数据库加载时长配置
     */
    protected static function loadDurationsFromDb(string $hospitalId): array
    {
        // TODO: 查询数据库
        // SQL: SELECT * FROM ect_hospital_durations WHERE hospital_id = ?
        
        return [
            '清洗' => 300,
            '消毒' => 300,
        ];
    }
}

5.2 使用加载器

use app\flow\config\DatabaseConfigLoader;

// 从数据库加载配置
$config = DatabaseConfigLoader::load('hospital_001');
$processor = new FlowProcessor($config);

六、自定义语音生成器

6.1 创建语音生成器

<?php

namespace app\flow\voice;

use app\flow\ProcessContext;

/**
 * 自定义语音生成器
 */
class CustomVoiceGenerator
{
    /**
     * 生成语音
     */
    public function generate(ProcessContext $context): string
    {
        $voice = '';

        // 根据医院配置选择语音模板
        switch ($context->processType) {
            case '手工洗':
                $voice = $this->generateManualWashVoice($context);
                break;
            case '机洗':
                $voice = $this->generateMachineWashVoice($context);
                break;
            default:
                $voice = $this->generateDefaultVoice($context);
        }

        // 添加内镜名称
        $name = str_replace(['北院', '南院', '电子'], '', $context->endoscopeName);
        return $name . $voice;
    }

    /**
     * 手工洗语音
     */
    protected function generateManualWashVoice(ProcessContext $context): string
    {
        $templates = [
            '清洗' => '手工清洗开始,请认真清洗',
            '漂洗' => '漂洗开始',
            '消毒' => '消毒开始,请确保消毒时间',
            '终末漂洗' => '终末漂洗开始',
            '干燥' => '干燥开始',
            '结束' => '手工清洗流程结束',
        ];

        return $templates[$context->currentStep] ?? $context->currentStep . '完成';
    }

    /**
     * 机洗语音
     */
    protected function generateMachineWashVoice(ProcessContext $context): string
    {
        $templates = [
            '机洗' => '机器清洗开始',
            '结束' => '机洗流程结束',
        ];

        return $templates[$context->currentStep] ?? $context->currentStep . '完成';
    }

    /**
     * 默认语音
     */
    protected function generateDefaultVoice(ProcessContext $context): string
    {
        return $context->currentStep . '完成';
    }
}

6.2 集成到策略

// 修改 VoiceGenerationStrategy
protected function generateNormalVoice(ProcessContext $context, ProcessNodeInterface $node): string
{
    // 使用自定义语音生成器
    $generator = new CustomVoiceGenerator();
    return $generator->generate($context);
}

七、自定义数据库操作

7.1 创建数据库处理器

<?php

namespace app\flow\db;

use app\flow\ProcessContext;

/**
 * 自定义数据库处理器
 */
class CustomDbHandler
{
    /**
     * 保存操作记录
     */
    public function save(ProcessContext $context): void
    {
        if ($context->dbOperation === 'insert') {
            $this->insertAction($context);
        } elseif ($context->dbOperation === 'update') {
            $this->updateAction($context);
        }
    }

    /**
     * 插入新记录
     */
    protected function insertAction(ProcessContext $context): void
    {
        // TODO: 实现插入逻辑
        $sql = "INSERT INTO ect_actions (
            batch_no, action_type, action_type_name, process_name,
            reader_id, reader_no, 
            opuser_type, opuser_id, opuser_rfid, opuser_name,
            endoscope_id, endoscope_rfid, endoscope_name,
            created_at, op_starttime
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";

        $params = [
            $context->batchNo,
            $this->getActionType($context->processType),
            $context->processType,
            $context->currentStep,
            $context->readerId,
            $context->readerNo,
            2, // 清洗人员
            $context->operatorId,
            $context->operatorRfid,
            $context->operatorName,
            $context->endoscopeId,
            $context->cardNo,
            $context->endoscopeName,
            date('Y-m-d H:i:s'),
            date('Y-m-d H:i:s'),
        ];

        // Database::execute($sql, $params);
    }

    /**
     * 更新记录
     */
    protected function updateAction(ProcessContext $context): void
    {
        // TODO: 实现更新逻辑
        $sql = "UPDATE ect_actions 
                SET op_endtime = ? 
                WHERE batch_no = ? 
                ORDER BY action_id DESC 
                LIMIT 1";

        // Database::execute($sql, [date('Y-m-d H:i:s'), $context->batchNo]);
    }

    /**
     * 获取操作类型编码
     */
    protected function getActionType(string $processType): int
    {
        $mapping = [
            '诊疗' => 0,
            '手工洗' => 1,
            '机洗' => 2,
            '测漏' => 7,
            '存储' => 8,
        ];

        return $mapping[$processType] ?? 1;
    }
}

7.2 集成到 FlowProcessor

// 修改 FlowProcessor::saveToDatabase()
protected function saveToDatabase(ProcessContext $context): void
{
    $handler = new CustomDbHandler();
    $handler->save($context);
}

八、扩展最佳实践

8.1 命名规范

  • 节点类:XxxNode,位于 app/flow/nodes/
  • 策略类:XxxStrategy,位于 app/flow/strategies/
  • 配置类:XxxConfig,位于 app/flow/config/

8.2 测试建议

// 单元测试示例
class PreprocessNodeTest
{
    public function testCanHandle()
    {
        $node = new PreprocessNode();
        
        $context = ProcessContext::create([
            'readerType' => '预处理',
            'currentStep' => '',
        ]);
        
        assert($node->canHandle($context) === true);
    }
}

8.3 调试技巧

// 打印流程上下文
var_dump($context);

// 打印责任链结构
$node = $engine->getChainHead();
while ($node !== null) {
    echo $node->getName() . ' -> ';
    $node = $node->getNext();
}

九、扩展示例汇总

扩展类型 难度 参考章节
添加新节点 2.1-2.4
添加新策略 3.1-3.3
修改节点行为 4.1-4.2
自定义配置加载 5.1-5.2
自定义语音 6.1-6.2
自定义数据库 7.1-7.2

十、常见问题

Q1: 如何调试新添加的节点?

doHandle() 方法中添加日志输出,查看上下文数据。

Q2: 策略执行顺序如何控制?

策略按照添加顺序执行,可以使用 setPhase('before'/'after') 控制执行阶段。

Q3: 如何禁用默认策略?

可以覆盖 attachStrategies() 方法或创建自定义引擎。

Q4: 如何处理数据库事务?

FlowProcessor::handleResult() 中添加事务控制逻辑。