标签搜索

策略模式在PHP实际开发中的应用

basil
2023-10-04 / 60 阅读

什么是策略模式

UML类图
策略模式是一种行为型设计模式,它定义了一系列算法,封装每个算法,并使它们可以相互替换。这意味着可以根据不同情况选择不同的算法,而不需要更改使用算法的对象的代码。
在策略模式中,有三个主要角色:

  1. 环境类(Context):它持有一个策略对象的引用,并在需要时调用策略对象的算法。
  2. 策略接口(Strategy):它定义了所有具体策略类必须实现的方法,它可以是抽象类或接口。
  3. 具体策略类(ConcreteStrategy):它实现了策略接口,具体实现了策略算法。

策略模式的优点是增强了代码的灵活性和可扩展性,可以动态切换算法,减少了各种算法类之间的耦合。同时,策略模式也符合开闭原则,易于理解和维护。

策略模式在数据同步中的使用案例

背景

公司在全国每个省份几乎都有经销商,每个经销商从公司采购产品,公司ERP系统销售出库单审核之后,需要同步到对应的经销商ERP系统的采购入库单,避免让经销商们自己手动录入,提高效率。虽然每个经销商用的都是跟总部一样的ERP系统,但是要同步的字段以及逻辑仍然会有差异,因此需要写一套代码可以根据不同的经销商灵活地切换同步的逻辑。

案例说明

UML类图

Test

  1. 首先定义策略接口(Strategy)SyncPurchaseInStockToErpInterface,包含getInstance方法(用于快速实例化策略类)、doSync方法(进行数据同步)、beautifyOrder方法(整理单据字段与格式)、beautifyMaterial方法(整理物料信息的字段与格式)。
  2. 定义具体策略类Adapter2Adapter3,对应ID为2和3的经销商,同时实现SyncPurchaseInStockToErpInterface策略接口,补充具体逻辑,因为不同的经销商单据传输的字段和数据有差异,可以在beautifyOrderbeautifyMaterial方法中进行特殊处理。
  3. 定义环境类SyncPurchaseInStockToErp,包含setAdapter方法(设置具体策略)、doSync方法(执行同步) ,根据不同的经销商调用不同的具体策略(Adapter)。
代码示例

SyncPurchaseInStockToErpInterface接口

   <?php


namespace app\command\agency\contract;


use app\command\agency\SyncPurchaseInStockToErp;

interface SyncPurchaseInStockToErpInterface
{

    /**
     * 获取当前实例
     * @return mixed
     */
    public static function getInstance();

    /**
     * 执行同步
     * @param SyncPurchaseInStockToErp $instance
     * @param $orders
     * @param $agencies
     * @return mixed
     */
    public function doSync(SyncPurchaseInStockToErp $instance, $orders, $agencies);

    /**
     * 格式化同步数据
     * @param $order··
     * @param $agencies
     * @return mixed
     */
    public function beautifyOrder($order, $agencies);

    /**
     * 格式化物料数据
     * @param $material
     * @return mixed
     */
    public function beautifyMaterial($material);
}

Adapter2

<?php


namespace app\command\agency\sync_purchase_in_stock_to_erp;


use app\command\agency\contract\SyncPurchaseInStockToErpInterface;
use app\command\agency\SyncPurchaseInStockToErp;
use app\library\enum\agency\BaseEnum;
use app\library\exception\ServiceLogicException;
use app\models\AgencyPurchaseInStock;
use app\models\AgencyPurchaseInStockPart;
use app\models\ErpBosAssistantDataDetail;
use app\models\ErpSaleOutStock;
use think\db\Query;

/**
 * 北京经销商
 * Class Adapter2
 * @package app\command\agency\sync_purchase_in_stock_to_erp
 */
class Adapter2 implements SyncPurchaseInStockToErpInterface
{
    private static $instance = null;

    // 经销商ERP组织编码
    const AGENCY_ORG_NUMBER = '100';

    /**
     * @var SyncPurchaseInStockToErp
     */
    protected $syncPurchaseInStockToErpInStance;

    private function __construct()
    {

    }

    private function __clone()
    {

    }

    public static function getInstance()
    {
        if (!self::$instance){
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * 执行同步
     * @param SyncPurchaseInStockToErp $instance 同步总部ERP出库单到经销商
     * @param $outStockOrder
     * @param $agencies
     * @return mixed|void
     * @throws ServiceLogicException
     * @throws \app\library\exception\ErpQueryException
     */
    public function doSync(SyncPurchaseInStockToErp $instance, $outStockOrder, $agencies)
    {
        $this->syncPurchaseInStockToErpInStance = $instance;
        $erpDataId = $agencies[$outStockOrder['customer_number']]['erp_data_id'] ?? '';
        if (!$erpDataId){
            throw new ServiceLogicException("出库单:{$outStockOrder['bill_no']}对应的经销商未配置ERP信息");
        }
        // 检查是否需要再次同步到经销商ERP
        if (!$instance->checkIsShouldSyncToAgencyErp($erpDataId, $outStockOrder)) return true;
        // 格式化采购入库单数据
        $data = $this->beautifyOrder($outStockOrder, $agencies);
        // 保存采购入库单到ERP
        $fid = $instance->savePurchaseInStockToErp($erpDataId, $data);
        // 更新门窗工匠的销售出库单明信息
        $instance->updateSaleOutStockEntry($erpDataId, $fid);
        // 更新门窗工匠的销售出库单信息
        ErpSaleOutStock::where('id', $outStockOrder['id'])->update(['is_async' => 1, 'is_async_hopo_erp' => 1]);
    }

    /**
     * 格式化采购入库单数据
     * @param $order
     * @param $agencies
     * @return array
     * @throws ServiceLogicException
     */
    public function beautifyOrder($order, $agencies)
    {
        if (empty($order)){
            return [];
        }
        $expressCompany = '';
        if ($order['express_company_id']){
            $expressCompany = ErpBosAssistantDataDetail::where('entry_id', $order['express_company_id'])->value('value') ?: '';
        }
        $saveData = [ 'IsDeleteEntry' => false, 'EnableApmTrace' => true ];
        $saveData['Model']['FSupplierId']['FNumber'] = 'VEN00001'; // 供应商
        $saveData['Model']['F_ZHR_ZBCKD'] = $order['bill_no']; // 总部出库单
        $saveData['Model']['F_ZHR_YSGS'] = $expressCompany; // 运输公司
        $saveData['Model']['F_ZHR_YSDH'] = $order['express_no']; // 运输单号
        $saveData['Model']['FDate'] = $order['create_date']; // 入库日期
        $erpDataId = $agencies[$order['customer_number']]['erp_data_id'];
        if (!isset($order['entry']) || empty($order['entry'])){
            throw new ServiceLogicException("出库单:{$order['bill_no']}缺少单据明细信息");
        }
        $FEntity = [];
        foreach ($order['entry'] as $item) {
            $this->syncPurchaseInStockToErpInStance->checkAgencyErpMaterial($erpDataId, $item['material'], self::AGENCY_ORG_NUMBER, 2);
            // 判断物料是否存在ERP或是否已审核
            // 获取历史价格
            $historyPrice = $diffAmount = 0;
            $FGiveAway = bccomp($item['tax_price'], 0, 2) <= 0;
            if (!$FGiveAway){ // 不是赠品
                $historyPrice = $this->getHistoryPrice($agencies[$order['customer_number']]['agency_id'], $item['material']);
                $diffAmount = $historyPrice ? bcsub($item['tax_price'], $historyPrice, 2) : 0; // 差异额
            }
            // 组装ERP参数
            $FEntity[] = [
                'FMaterialId'       => [ 'FNumber' => $item['material'] ],
                'FStockId'          => [ 'FNumber' => BaseEnum::AGENCY_STOCK[$order['customer_number']] ],
                'FStockLocId'       => ['FSTOCKLOCID__FF100001' => ['FNumber' => 'A01']],
                'FRemainInStockQty' => $item['real_qty'],
                'FRealQty'          => $item['real_qty'],
                'FPriceUnitQty'     => $item['real_qty'],
                'FTaxPrice'         => $item['tax_price'],
                'F_WH_BOASSFID'     => $order['fid'],
                'F_WH_BOASSFENTRY'  => $item['entry_id'],
                'F_WH_FHFID'        => $order['entity_link_bill_id'],
                'F_WH_FHFENTRY'     => $item['entity_link_id'],
                'F_wh_LSPrice'      => $historyPrice,
                'F_WH_CY'           => $diffAmount,
                'FGiveAway'         => $FGiveAway,
                'FNote'             => "总部" . date("Y-m-d", strtotime($order['create_date']))
            ];
        }
        $saveData['Model']['FInStockEntry'] = $FEntity;
        return $saveData;
    }

    /**
     * 格式化物料数据
     * @param $materialNumber
     * @return array
     * @throws ServiceLogicException
     */
    public function beautifyMaterial($materialNumber)
    {
        $boassErpMaterial = $this->syncPurchaseInStockToErpInStance->getMaterial($materialNumber);
        $jsonData = [];
        $jsonData['FCreateOrgId']     = '001'; //创建者 非必录
        $jsonData['NeedUpDateFields'] = []; //需要保存的字段,格式["fieldkey1","fieldkey2","entitykey1",...],数组类型(非必录)
        $jsonData['NeedReturnFields'] = []; //需要返回的结果字段,格式["fieldkey","entitykey.fieldkey",...](非必录)
        $model['FNumber']        = $boassErpMaterial->product_sn; // 物料编码
        $model['FName']          = $boassErpMaterial->product_name; // 物料名称
        $model['F_SQ_BZXNSL']    = $boassErpMaterial->container_amount;  // 包装箱内数量
        $model['F_SQ_BZHNSL1']   = $boassErpMaterial->box_amount;  // 包装盒内数量
        $model['FSpecification'] = $boassErpMaterial->specification;  // 规格型号
        $model['SubHeadEntity'] = [
            'FErpClsID'   => '1', // 物料属性
//            'FBaseUnitId' => ['FNumber' => $boassErpMaterial->unit_number] , // 物料单位
            'FCategoryID' => ['FNumber' => 'CHLB05_SYS'] // 存货类别
        ];
        $model['SubHeadEntity1'] = [
            'FStockId' => ['FNumber' => 'CK001']
        ];
        $jsonData['Model'] = $model;
        return $jsonData;
    }

    /**
     * 获取历史价格
     * @param $agencyId
     * @param $materialNumber
     * @return int|mixed
     */
    protected function getHistoryPrice($agencyId, $materialNumber)
    {
        $inStockDate = BaseEnum::AGENCY_HISTORY_PURCHASE_IN_STOCK_DATE[$agencyId] ?? '2022-08-01';
        $taxPrice = AgencyPurchaseInStockPart::alias('apisp')
            ->leftJoin(AgencyPurchaseInStock::getTable() . ' apis', 'apisp.purchase_in_stock_id = apis.id')
            ->where('apisp.agency_id', $agencyId)
            ->where('apisp.material_number', $materialNumber)
            ->where(function (Query $query) use ($inStockDate){
                $query->where('apis.document_status', 'C')
                    ->whereOr('apis.in_stock_date', $inStockDate);
            })
            ->where('apisp.tax_price', '<>', 0)
            ->order('apisp.entry_id', 'desc')->value('tax_price');
        return $taxPrice ?: 0;
    }

}

Adapter3

<?php


namespace app\command\agency\sync_purchase_in_stock_to_erp;


use app\command\agency\contract\SyncPurchaseInStockToErpInterface;
use app\command\agency\SyncPurchaseInStockToErp;
use app\library\enum\agency\BaseEnum;
use app\library\exception\ServiceLogicException;
use app\models\AgencyPurchaseInStock;
use app\models\AgencyPurchaseInStockPart;
use app\models\ErpSaleOutStock;
use app\service\agency\erp\ErpQuery;
use think\db\Query;

/**
 * 河南经销商
 * Class Adapter1539
 * @package app\command\agency\sync_purchase_in_stock_to_erp
 */
class Adapter3 implements SyncPurchaseInStockToErpInterface
{
    private static $instance = null;

    // 经销商ERP组织编码
    const AGENCY_ORG_NUMBER = '009';

    /**
     * @var SyncPurchaseInStockToErp
     */
    protected $syncPurchaseInStockToErpInStance;

    private function __construct()
    {

    }

    private function __clone()
    {

    }

    public static function getInstance()
    {
        if (!self::$instance){
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * 执行同步
     * @param SyncPurchaseInStockToErp $instance 同步总部ERP出库单到经销商
     * @param $outStockOrder
     * @param $agencies
     * @return mixed|void
     * @throws ServiceLogicException
     */
    public function doSync(SyncPurchaseInStockToErp $instance, $outStockOrder, $agencies)
    {
        $this->syncPurchaseInStockToErpInStance = $instance;
        $erpDataId = $agencies[$outStockOrder['customer_number']]['erp_data_id'] ?? '';
        if (!$erpDataId){
            throw new ServiceLogicException("出库单:{$outStockOrder['bill_no']}对应的经销商未配置ERP信息");
        }
        // 检查是否需要再次同步到经销商ERP
        if (!$instance->checkIsShouldSyncToAgencyErp($erpDataId, $outStockOrder)) return true;
        // 格式化采购入库单数据
        $data = $this->beautifyOrder($outStockOrder, $agencies);
        // 保存采购入库单到ERP
        $fid = $instance->savePurchaseInStockToErp($erpDataId, $data);
        // 更新门窗工匠的销售出库单明信息
        $instance->updateSaleOutStockEntry($erpDataId, $fid);
        // 更新门窗工匠的销售出库单信息
        ErpSaleOutStock::where('id', $outStockOrder['id'])->update(['is_async' => 1, 'is_async_hopo_erp' => 1]);
    }

    /**
     * 格式化采购入库单数据
     * @param $order
     * @param $agencies
     * @return array
     * @throws ServiceLogicException
     */
    public function beautifyOrder($order, $agencies)
    {
        if (empty($order)){
            return [];
        }
        $saveData = [ 'IsDeleteEntry' => false, 'EnableApmTrace' => true ];
        // !!!采购组织一定要放在供应商前面,不然会报缺少供应商字段
        $saveData['Model']['FStockOrgId']['FNumber'] = self::AGENCY_ORG_NUMBER; // 收料组织
        $saveData['Model']['FPurchaseOrgId']['FNumber'] = self::AGENCY_ORG_NUMBER; // 采购组织
        $saveData['Model']['FSupplierId']['FNumber'] = 'VEN00025'; // 供应商
        $saveData['Model']['FOwnerIdHead']['FNumber'] = self::AGENCY_ORG_NUMBER; // 货主
        $saveData['Model']['FDate'] = $order['create_date']; // 入库日期
        if (!isset($order['entry']) || empty($order['entry'])){
            throw new ServiceLogicException("出库单:{$order['bill_no']}缺少单据明细信息");
        }
        $erpDataId = $agencies[$order['customer_number']]['erp_data_id'];
        $FEntity = [];
        foreach ($order['entry'] as $item) {
            $materialId = $this->syncPurchaseInStockToErpInStance->checkAgencyErpMaterial($erpDataId, $item['material'], self::AGENCY_ORG_NUMBER);
            $material = ErpQuery::setDataId($erpDataId)
                ->form('BD_MATERIAL')
                ->field("FBaseUnitId.FNumber")
                ->where("FMATERIALID = '{$materialId}'")
                ->page(1, 1)
                ->select();
            if (empty($material)) throw new ServiceLogicException('物料不存在');
            $unit = $material[0][0];
            // 判断物料是否存在ERP或是否已审核
            // 获取历史价格
            $historyPrice = $diffAmount = 0;
            $FGiveAway = bccomp($item['tax_price'], 0, 2) <= 0;
            if (!$FGiveAway){ // 不是赠品
                $historyPrice = $this->getHistoryPrice($agencies[$order['customer_number']]['agency_id'], $item['material']);
                $diffAmount = $historyPrice ? bcsub($item['tax_price'], $historyPrice, 2) : 0; // 差异额
            }
            // 组装ERP参数
            $FEntity[] = [
                'FOWNERTYPEID'      => 'BD_OwnerOrg',
                'FOWNERID'          => ['FNumber' => self::AGENCY_ORG_NUMBER], // 货主
                'FMaterialId'       => [ 'FNumber' => $item['material'] ],
                'FUnitID'           => ['FNumber' => $unit],
                'FPriceUnitID'      => ['FNumber' => $unit],
                'FRemainInStockUnitId' => ['FNumber' => $unit],
                'FStockId'          => [ 'FNumber' => BaseEnum::AGENCY_STOCK[$order['customer_number']] ],
                'FStockStatusId'    => ['FNumber' => 'KCZT01_SYS'],
                'FRemainInStockQty' => $item['real_qty'],
                'FRealQty'          => $item['real_qty'],
                'FPriceUnitQty'     => $item['real_qty'],
                'FTaxPrice'         => $item['tax_price'],
                'F_WH_BOASSFID'     => $order['fid'],
                'F_WH_BOASSFENTRY'  => $item['entry_id'],
                'F_WH_FHFID'        => $order['entity_link_bill_id'],
                'F_WH_FHFENTRY'     => $item['entity_link_id'],
                'F_wh_LSPrice'      => $historyPrice,
                'F_WH_CY'           => $diffAmount,
                'FGiveAway'         => $FGiveAway
            ];
        }
        $saveData['Model']['FInStockEntry'] = $FEntity;
        return $saveData;
    }

    /**
     * 格式化物料数据
     * @param $materialNumber
     * @return array
     * @throws ServiceLogicException
     */
    public function beautifyMaterial($materialNumber)
    {
        $boassErpMaterial = $this->syncPurchaseInStockToErpInStance->getMaterial($materialNumber);
        $jsonData = [];
        $jsonData['NeedUpDateFields'] = []; //需要保存的字段,格式["fieldkey1","fieldkey2","entitykey1",...],数组类型(非必录)
        $jsonData['NeedReturnFields'] = []; //需要返回的结果字段,格式["fieldkey","entitykey.fieldkey",...](非必录)
        $model['FCreateOrgId']     = ['FNumber' => self::AGENCY_ORG_NUMBER]; //创建组织
        $model['FUseOrgId']        = ['FNumber' => self::AGENCY_ORG_NUMBER]; // 使用组织
        $model['FNumber']        = $boassErpMaterial->product_sn; // 物料编码
        $model['FName']          = $boassErpMaterial->product_name; // 物料名称
        $model['F_SQ_BZXNSL']    = $boassErpMaterial->container_amount;  // 包装箱内数量
        $model['F_SQ_BZHNSL1']   = $boassErpMaterial->box_amount;  // 包装盒内数量
        $model['FSpecification'] = $boassErpMaterial->specification;  // 规格型号
        $model['SubHeadEntity'] = [
            'FErpClsID'   => '1', // 物料属性
            'FBaseUnitId' => ['FNumber' => $boassErpMaterial->unit_number] , // 物料单位
            'FCategoryID' => ['FNumber' => 'CHLB05_SYS'] // 存货类别
        ];
        $model['SubHeadEntity1'] = [
            'FStockId' => ['FNumber' => 'CK001']
        ];
        $jsonData['Model'] = $model;
        return $jsonData;
    }

    /**
     * 获取历史价格
     * @param $agencyId
     * @param $materialNumber
     * @return int|mixed
     */
    protected function getHistoryPrice($agencyId, $materialNumber)
    {
        $inStockDate = BaseEnum::AGENCY_HISTORY_PURCHASE_IN_STOCK_DATE[$agencyId] ?? '2022-08-01';
        $taxPrice = AgencyPurchaseInStockPart::alias('apisp')
            ->leftJoin(AgencyPurchaseInStock::getTable() . ' apis', 'apisp.purchase_in_stock_id = apis.id')
            ->where('apisp.agency_id', $agencyId)
            ->where('apisp.material_number', $materialNumber)
            ->where(function (Query $query) use ($inStockDate){
                $query->where('apis.document_status', 'C')
                    ->whereOr('apis.in_stock_date', $inStockDate);
            })
            ->where('apisp.tax_price', '<>', 0)
            ->order('apisp.entry_id', 'desc')->value('tax_price');
        return $taxPrice ?: 0;
    }

}

SyncPurchaseInStockToErp

<?php

namespace app\command\agency;

use app\admin\service\MaterialService;
use app\command\agency\contract\SyncPurchaseInStockToErpInterface;
use app\library\enum\agency\AgencyDataSyncEnum;
use app\library\enum\agency\BaseEnum;
use app\library\exception\ServiceLogicException;
use app\models\AgencyDatabase;
use app\models\ErpMaterial;
use app\models\ErpSaleOutStock;
use app\models\ErpSaleOutStockEntry;
use app\models\UserInfo;
use app\service\agency\erp\ErpQuery;
use app\service\notice\YunZhiJiaNoticeRobot;
use think\console\input\Option;
use think\db\Query;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;
use app\common\MyLog;
use think\facade\Cache;

/**
 * 同步总部销售出库单到经销商ERP的采购入库单
 */
class SyncPurchaseInStockToErp extends Command
{
    /**
     * @var SyncPurchaseInStockToErpInterface
     */
    protected $adapter;

    protected function configure() 
    {
        $this->setName('SyncPurchaseInStockToErp')
            ->addOption('agency_id', null, Option::VALUE_REQUIRED, "经销商ID,多个以英文逗号隔开")
            ->addOption('create_date', null, Option::VALUE_REQUIRED, "创建日期")
            ->addArgument('id', Argument::OPTIONAL, 0)
            ->setDescription('同步总部ERP销售出库单信息到经销商ERP采购入库单(新版)');
    }

    protected function execute(Input $input, Output $output) 
    {
        $id = trim($input->getArgument('id'));
        $agencyIds = $input->getOption('agency_id') ?: '469';
        $createDate = $input->getOption('create_date') ?: '';
        // 获取经销商ERP信息
        $agencies = $this->getAgency($agencyIds);
        foreach ($agencies as $agencyErpSn => $agency) {
            try {
                // 获取需要同步的采购入库单
                $createDate = AgencyDataSyncEnum::AGENCY_SYNC_PURCHASE_IN_STOCK_ORDER_DATE[$agency['agency_id']] ?? '';
                if (!$createDate){
                    continue;
                }
                $outStockOrders = $this->getOutStockOrder(['create_date' => $createDate, 'agency_sn' => $agencyErpSn, 'id' => $id]);
                $this->doSync($agencies, $outStockOrders);
            }catch (\Exception $e){
                MyLog::errorLog("同步总部出库单到经销商ERP采购入库单发生异常,经销商ID:{$agency['agency_id']}", $e->getMessage(), 'rsync_agency_purchase_in_stock');
            }
        }
        $output->writeln('商品数据成功同步至经销商ERP!');
    }

    /**
     * 执行同步
     * @param $agencies array 经销商信息
     * @param $outStockOrders array 出库单信息
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    protected function doSync($agencies, $outStockOrders)
    {
        foreach ($outStockOrders as $outStockOrder) {
            $agencyId = $agencies[$outStockOrder['customer_number']]['agency_id'] ?? 0;
            try {
                if (!$agencyId){
                    throw new ServiceLogicException("对应的经销商未配置agency_id");
                }
                $className = "app\\command\\agency\\sync_purchase_in_stock_to_erp\\Adapter{$agencyId}";
                $adapter = $className::getInstance();
                MyLog::infoLog('销售出库单信息', $outStockOrder, 'rsync_agency_purchase_in_stock');
                $adapter->doSync((new self())->setAdapter($adapter), $outStockOrder, $agencies);
                MyLog::infoLog('success', "同步成功:{$outStockOrder['bill_no']}", 'rsync_agency_purchase_in_stock');
            }catch (\Exception $e){
                $agencyName = UserInfo::where('user_id', $agencyId)->value('company_name');
                $msg = "{$agencyName}出库单{$outStockOrder['bill_no']}同步到经销商ERP采购入库单失败,报错信息:" . $e->getMessage() . ' 位于文件:' . $e->getFile() . ' 第' . $e->getLine() . '行';
                MyLog::errorLog('error', $msg, 'rsync_agency_purchase_in_stock');
                YunZhiJiaNoticeRobot::sendSyncDataExceptionMsg($msg);
                continue;
            }
        }
    }

    public function setAdapter(SyncPurchaseInStockToErpInterface $adapter)
    {
        $this->adapter = $adapter;
        return $this;
    }

    /**
     * 获取经销商信息
     * @param $agencyIds
     * @return array
     * @throws ServiceLogicException
     */
    protected function getAgency($agencyIds)
    {
        $query = AgencyDatabase::where('status', 1)
            ->where('erp_sn', '<>', '');
        $agencyIds && $query->where('agency_id', 'in', explode(',', $agencyIds));
        $agencies = $query->field('erp_sn,erp_data_id,agency_id')
            ->select()
            ->toArray();
        if (empty($agencies)){
            throw new ServiceLogicException('经销商ERP信息未在数据库配置');
        }
        return array_column($agencies, null, 'erp_sn');
    }

    /**
     * 获取总部销售出库单信息
     * @param $params
     * @return array|array[]|\array[][]|\array[][][]
     */
    protected function getOutStockOrder($params)
    {
        $createDate = $params['create_date'] ?? '';
        $id = $params['id'] ?? 0;
        $where = [];
        $where[] = ['is_new','=',1];
        $where[] = ['is_async','=',0];
        $where[] = ['is_replacement_order', '=', 0];
        $where[] = ['customer_number', '=', $params['agency_sn']];
        $createDate && $where[] = ['create_date', '>=', $createDate];
        $id && $where[] = ['id', '=', $id];
        return ErpSaleOutStock::where($where)
            ->field('id,customer_number,fid,bill_no,entity_link_bill_id,express_no,express_company_id,approve_date,create_date,head_location_number')
            ->with([
                'entry' => function(Query $query){
                    $query->order('entry_id asc');
                }
            ])
            ->limit(10)
            ->select()
            ->toArray();
    }

    /**
     * 检查是否需要同步到经销商采购入库单
     * @param $erpDataId
     * @param $order
     * @return bool
     * @throws ServiceLogicException
     * @throws \app\library\exception\ErpQueryException
     */
    public function checkIsShouldSyncToAgencyErp($erpDataId, $saleOutStockOrder)
    {
        $fhFid = Cache::store('redis')->get('fhFid');
        if ($fhFid && $fhFid == $saleOutStockOrder['entity_link_bill_id']){
            Cache::store('redis')->rm('fhFid');
            return true;
        }
        $order = ErpQuery::setDataId($erpDataId)
            ->form('STK_InStock')
            ->where("F_WH_FHFID = '{$saleOutStockOrder['entity_link_bill_id']}'")
            ->where("F_WH_BOASSFID = '{$saleOutStockOrder['fid']}'")
            ->field("FID,FDocumentStatus")
            ->select();
        if (empty($order)){
            return true;
        }
        if ($order[0][1] != 'A'){ // 不是创建状态不用再次同步到ERP
            ErpSaleOutStock::where('id', $saleOutStockOrder['id'])->update(['is_async' => 1, 'is_async_hopo_erp' => 1]);
            return false;
        }
        // 如果已经存在创建状态的采购入库单,则把它删除
        $this->deletePurchaseInStockInErp($erpDataId, $order[0][0]);
        return true;
    }


    /**
     * 检查经销商ERP物料信息是否存在并保存、提交、审核
     * @param $erpDataId string ERP账套ID
     * @param $materialNumber string 物料编码
     * @param string $useOrgNumber 组织编码
     * @return mixed
     * @throws ServiceLogicException
     * @throws \app\library\exception\ErpQueryException
     */
    public function checkAgencyErpMaterial($erpDataId, $materialNumber, $useOrgNumber = '', $agencyId = 0)
    {
        $query = ErpQuery::setDataId($erpDataId)
            ->form('BD_MATERIAL')
            ->where("FNumber = '$materialNumber'")
            ->field("FMATERIALID,FNumber,FName,FDocumentStatus,FForbidStatus,FCreateOrgId,FCreateOrgId.FNumber,FUseOrgId,FUseOrgId.FNumber");
        $material = $query->order("FMATERIALID desc")
            ->select();
        if (empty($material)){ // 如果当前账套所有组织都不存在此物料,则使用当前组织创建
            $id = $this->saveMaterialToErp($erpDataId, $materialNumber);
            $this->submitMaterialInErp($erpDataId, $id);
            $this->auditMaterialInErp($erpDataId, $id);
            return $id;
        }
        $createOrgMaterialId = $this->getCreateOrgMaterialId($material); // 创建组织的物料ID
        $useOrgNumberKey = 8;
        $materialMap = array_column($material, null, $useOrgNumberKey);
        if (!isset($materialMap[$useOrgNumber])){ // 如果当前组织没有被分配,则进行分配(因为同一个账套不同组织不能拥有相同编码,只能通过分配的方式解决)
            $id = $this->allocateMaterialInErp($erpDataId, $createOrgMaterialId, BaseEnum::AGENCY_ERP_ORG_FID_MAP[$agencyId][$useOrgNumber]);
            $useOrgMaterial = ErpQuery::setDataId($erpDataId)
                ->form('BD_MATERIAL')
                ->field("FMaterialId")
                ->where("FUseOrgId.FNumber = '$useOrgNumber'")
                ->where("FNumber = '$materialNumber'")
                ->page(1, 1)
                ->select();
            if (empty($useOrgMaterial)) throw new ServiceLogicException("分配物料:$materialNumber 后在ERP查找不到");
            $this->submitMaterialInErp($erpDataId, $useOrgMaterial[0][0]);
            $this->auditMaterialInErp($erpDataId, $useOrgMaterial[0][0]);
            return $id;
        }
        $material = $materialMap[$useOrgNumber];
        $documentStatus = $material[3];
        $forbidStatus = $material[4];
        $id = $material[0];
        if ($forbidStatus == 'B'){ // 如果被禁用则进行反禁用
            $this->enableMaterialInErp($erpDataId, $material[0]);
        }
        // 如果物料的状态为创建或重新审核或暂存,则提交、审核
        if ($documentStatus == 'A' || $documentStatus == 'D' || $documentStatus == 'Z'){
            $this->submitMaterialInErp($erpDataId, $id);
            $this->auditMaterialInErp($erpDataId, $id);
        }
        // 如果在审核中,直接提交审核
        if ($documentStatus == 'B'){
            $this->auditMaterialInErp($erpDataId, $id);
        }
        return $id;
    }

    protected function getCreateOrgMaterialId($materials)
    {
        foreach ($materials as $material) {
            if ($material[5] == $material[7]){
                return $material[0];
            }
        }
        return $materials[0][0];
    }

    /**
     * 获取门窗工匠的物料信息
     * @param $materialNumber
     * @return ErpMaterial
     * @throws ServiceLogicException
     */
    public function getMaterial($materialNumber)
    {
        $boassErpMaterial = ErpMaterial::where('product_sn', $materialNumber)
            ->field('product_sn,product_name,container_amount,box_amount,specification,unit_number')
            ->find();
        if (!$boassErpMaterial){
            MaterialService::getInstance()->syncMaterialByFNumber($materialNumber, 'system');
            $boassErpMaterial = ErpMaterial::where('product_sn', $materialNumber)
                ->field('product_sn,product_name,container_amount,box_amount,specification,unit_number')
                ->find();
        }
        if (!$boassErpMaterial){
            throw new ServiceLogicException("创建物料失败:{$materialNumber},没有在门窗工匠中找到该物料信息");
        }
        return $boassErpMaterial;
    }

    /**
     * 保存物料到经销商ERP
     * @param $data
     * @return mixed
     * @throws ServiceLogicException
     */
    public function saveMaterialToErp($erpDataId, $materialNumber)
    {
        $jsonData = $this->adapter->beautifyMaterial($materialNumber);
        $result = ErpQuery::setDataId($erpDataId)
            ->form('BD_MATERIAL')
            ->save($jsonData);
        $isSuccess = $result['content']['Result']['ResponseStatus']['IsSuccess'] ?? false;
        if (!$isSuccess){
            $error = $result['content']['Result']['ResponseStatus']['Errors'][0]['Message'] ?? '未知错误';
            throw new ServiceLogicException( "保存物料失败:" . $error);
        }
        return $result['content']['Result']['Id'];
    }

    /**
     * 提交物料信息
     * @param $erpDataId
     * @param $ids
     * @throws ServiceLogicException
     */
    public function submitMaterialInErp($erpDataId, $ids)
    {
        $result = ErpQuery::setDataId($erpDataId)
            ->form('BD_MATERIAL')
            ->submit(['Ids' => $ids]);
        $isSuccess = $result['content']['Result']['ResponseStatus']['IsSuccess'] ?? false;
        if (!$isSuccess){
            $error = $result['content']['Result']['ResponseStatus']['Errors'][0]['Message'] ?? '未知错误';
            throw new ServiceLogicException("提交物料失败:" . $error);
        }
    }

    /**
     * 审核物料信息
     * @param $erpDataId
     * @param $ids
     * @throws ServiceLogicException
     */
    public function auditMaterialInErp($erpDataId, $ids)
    {
        $result = ErpQuery::setDataId($erpDataId)
            ->form('BD_MATERIAL')
            ->audit(['Ids' => $ids]);
        $isSuccess = $result['content']['Result']['ResponseStatus']['IsSuccess'] ?? false;
        if (!$isSuccess){
            $error =  $result['content']['Result']['ResponseStatus']['Errors'][0]['Message'] ?? '未知错误';
            throw new ServiceLogicException( "审核物料失败:" . $error);
        }
    }

    /**
     * 分配物料
     * @param $erpDataId
     * @param $ids string 物料ID,多个使用英文逗号隔开
     * @param $orgIds string 组织ID字符串,多个使用英文逗号隔开
     * @throws ServiceLogicException
     */
    public function allocateMaterialInErp($erpDataId, $ids, $orgIds)
    {
        $result = ErpQuery::setDataId($erpDataId)
            ->form('BD_MATERIAL')
            ->allocate(['PkIds' => $ids, 'TOrgIds' => $orgIds]);
        $isSuccess = $result['content']['Result']['ResponseStatus']['IsSuccess'] ?? false;
        if (!$isSuccess){
            $error =  $result['content']['Result']['ResponseStatus']['Errors'][0]['Message'] ?? '未知错误';
            throw new ServiceLogicException( "分配物料失败:" . $error);
        }
        return $result['content']['Result']['ResponseStatus']['SuccessEntitys'][0]['Id'];
    }

    /**
     * 反禁用物料
     * @param $erpDataId
     * @param $ids
     * @throws ServiceLogicException
     */
    public function enableMaterialInErp($erpDataId, $ids)
    {
        $result = ErpQuery::setDataId($erpDataId)
            ->form('BD_MATERIAL')
            ->enable(['Ids' => $ids]);
        $isSuccess = $result['content']['Result']['ResponseStatus']['IsSuccess'] ?? false;
        if (!$isSuccess){
            $error =  $result['content']['Result']['ResponseStatus']['Errors'][0]['Message'] ?? '未知错误';
            throw new ServiceLogicException( "反禁用物料失败:" . $error);
        }
    }

    /**
     * 删除经销商采购入库单
     * @param $erpDataId
     * @param $ids
     * @throws ServiceLogicException
     */
    public function deletePurchaseInStockInErp($erpDataId, $ids)
    {
        $result = ErpQuery::setDataId($erpDataId)
            ->form('STK_InStock')
            ->delete(['Ids' => $ids]);
        $isSuccess = $result['content']['Result']['ResponseStatus']['IsSuccess'] ?? false;
        if (!$isSuccess){
            $error =  $result['content']['Result']['ResponseStatus']['Errors'][0]['Message'] ?? '未知错误';
            throw new ServiceLogicException( "删除失败:" . $error);
        }
    }

    /**
     * 保存采购入库单
     * @param $erpDataId
     * @param $data
     * @return mixed
     * @throws ServiceLogicException
     */
    public function savePurchaseInStockToErp($erpDataId, $data)
    {
        $result = ErpQuery::setDataId($erpDataId)
            ->form('STK_InStock')
            ->save($data);
        $isSuccess = $result['content']['Result']['ResponseStatus']['IsSuccess'] ?? false;
        if (!$isSuccess){
            $error =  $result['content']['Result']['ResponseStatus']['Errors'] ?? '[]';
            throw new ServiceLogicException( "保存失败:" . json_encode($error, JSON_UNESCAPED_UNICODE) .  "dataId:{$erpDataId}");
        }
        return $result['content']['Result']['Id'];
    }

    /**
     * 更新门窗工匠出库单明细
     * @param $erpDataId
     * @param $fid
     * @throws ServiceLogicException
     * @throws \app\library\exception\ErpQueryException
     */
    public function updateSaleOutStockEntry($erpDataId, $fid)
    {
        $saleOutStockEntry = ErpQuery::setDataId($erpDataId)
            ->form('STK_InStock')
            ->where("FID = '$fid'")
            ->field("FID,FBillNo,FInStockEntry_FEntryID,FMaterialId.FNumber,F_WH_BOASSFID,F_WH_BOASSFENTRY,FRemainInStockQty")
            ->select();
        if (empty($saleOutStockEntry)){
            throw new ServiceLogicException('获取经销商采购入库单明细信息失败');
        }
        foreach ($saleOutStockEntry as $item) {
            $entry = ErpSaleOutStockEntry::where('entry_id', $item[5])
                ->where('material', $item[3])
                ->field('id,material,real_qty,agency_purchase_in_stock_num,agency_purchase_in_stock_fid,agency_purchase_in_stock_entry_id')
                ->find();
            if (!$entry){
                throw new ServiceLogicException("未找到总部出库单明细信息entry_id:{$item[5]}");
            }
            $entry->agency_purchase_in_stock_status = 1;
            $inStockNum = $item[6];
            if ($inStockNum >= $entry->real_qty) {
                $entry->agency_purchase_in_stock_status = 2;
            }
            $entry->agency_purchase_in_stock_num = $inStockNum;
            $entry->agency_purchase_in_stock_no = $item[1];
            $entry->agency_purchase_in_stock_fid = $item[0];
            $entry->agency_purchase_in_stock_entry_id = $item[2];
            $entry->save();
        }
    }

}
0