首页
关于
Search
1
阿里云rds数据库mysql版cpu占用100%排查问题过程
1,191 阅读
2
解决Gitlab进行clone、push、pull的时候报错aborting due to possible repository corruption on the remote side. git-pack-objects died with error.index-pack failed问题
1,121 阅读
3
nginx、php-fpm、thinkphp接口请求偶尔返回502导致前端报CORS跨域错误问题
715 阅读
4
使用VMware Workstation pro 15安装黑苹果后,开机卡在logo的问题
581 阅读
5
PHP连接SQLserver报错:SQLSTATE[IMSSP]: This extension requires the Microsoft ODBC Driver for SQL Server to communicate with SQL Server. Access the followin
492 阅读
计算机
数据库
Linux
PHP开发
前端
好文收藏
产品
创业
天天向上
阅读
工作
登录
Search
标签搜索
PHP
ss
pdo
mysql
php8
阅读
摘抄
PHP后端开发技术学习
累计撰写
104
篇文章
累计收到
1
条评论
首页
栏目
计算机
数据库
Linux
PHP开发
前端
好文收藏
产品
创业
天天向上
阅读
工作
页面
关于
搜索到
69
篇与
PHP开发
的结果
2024-10-30
NGINX配置静态文件一直报404错误
NGINX配置静态文件一直报404错误
2024年10月30日
6 阅读
2024-04-09
金蝶ERP与数据中心同步数据方案
为什么要建设中间件?不能完全去掉金蝶ERP公司早期没有上计划、采购、生产、仓库等系统,都在ERP上面进行,因此基础资料、计划、采购、生产、销售、仓库、财务等数据都在ERP上,数据比较完整且准确虽然上线VPS、MES、WMS、SRM、QMS系统后,计划、生产、采购、仓库可以使用这些系统代替,但是仍然需要依赖金蝶ERP的MRP运算以及财务模块将ERP作为结果数据的汇集中心,将各个系统的产生结果数据回写到ERP,将ERP作为公司各个业务模块数据分析报表的数据来源。需要搭建供应链以及销售端相关系统与ERP数据同步的桥梁为什么不直接连接ERP数据库读取和写入数据ERP没有本地化,只能通过接口的方式读写数据初期ERP托管在金蝶的云服务,考虑成本预算问题,并没有买断部署到本地,因此没有数据库的操作权限,只能通过金蝶ERP的webapi接口进行同步。直接写入数据到ERP数据库需要对ERP内部逻辑完全熟悉直接写入数据到erp数据这种方式会对ERP系统形成入侵,开发工作量巨大,同时一不小心就会影响ERP运行,是不合理的方式。实现系统间数据同步统一管控避免每个系统重复开发同步功能,节省开发成本方便对数据同步进行统一管理,例如可视化、异常处理、限流等等怎么建设中间件?架构 基于上述的背景,中间件的架构如上图,它的作用主要是通过api的方式定时从ERP拉取数据到中间件数据库,其它系统从中间件数据库读取数据,回写数据时,其它系统通过中间件的接口投递消息到回写队列,回写队列再通过ERP的api同步数据到ERP遇到的问题与解决方案ERP数据删除操作是物理删除因为是物理删除,涉及的单据多,没有删除通知功能,其它系统无法知道ERP删除了单据,加上没有数据库权限,不能添加触发器,所以一开始找了金蝶ERP的工程师开发了插件,把删除的数据标记起来漏数据第三方插件不完善增量时间不触发分页问题数据同频问题(vps)erp触发全量数据更新,同步很慢同一个单由于事务原因以及多次获取数据的原因,导致部分行没有同步到下游系统批量投放生产订单,同时产生的生产用料清单数据量很大,同步时每次请求erp查询时间很长,最后更新字段也要加索引优化解决办法本地部署ERP打触发器不分页获取
2024年04月09日
78 阅读
2024-01-02
workerman+websocket实现公众号扫码关注登录
背景为了方面用户登录系统以及引导用户关注公众号,需要做一个微信扫码登录的功能实现逻辑说明用到的技术或框架websocket thinkphp5.1 workerman3.5.0 overtrue/wechat时序图 说明 1.用户访问登录页面后,web端请求获取二维码生成接口 2.登录服务接收到二维码生成请求后,生成loginId,然后携带loginId请求微信二维码生成接口获取二维码URL,最后将二维码URL以及loginId返回给web端 3.web端获取到二维码URL和loginId后,显示二维码并携带loginId与登录服务建立websocket连接 4.用户扫描后关注并跳转到微信公众号,同时触发微信的事件消息回调,将用户openid返回给登录服务 5.登录服务执行登录逻辑后,将登录结果通过websocket返回到web端,web端再跳转到登录成功或账号绑定界面关键代码获取微信二维码public function getLoginQrCode() { $app = Factory::officialAccount(Config::get('official_account.')); $loginId = uniqid(); $params = [ 'scene' => 'login', 'login_id' => $loginId, 'staff_id' => Request::param('staff_id') ?? '' ]; $content = $app->qrcode->temporary(http_build_query($params)); Cache::store('redis')->set($loginId, 'true', 120); $content['login_id'] = $params['login_id']; isset($content['ticket']) && $content['picture_url'] = 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' . $content['ticket']; throw new SuccessException(['content' => $content]); }workerman实现websocket协议,并创建一个内部的http协议服务接收消息<?php namespace app\service\workerman; use app\common\MyLog; use think\facade\Cache; use think\facade\Config; use think\facade\Env; use think\worker\Server; use Workerman\Connection\TcpConnection; use Workerman\Lib\Timer; use Workerman\Worker; use app\facade\model\InstantMessageLog; class WebsocketServer extends Server { protected $socket = ''; protected $context = [ 'ssl' => [ // 请使用绝对路径 'local_cert' => '', // pem文件也可以是crt文件 'local_pk' => '', // key文件 'verify_peer' => false, 'allow_self_signed' => true, //如果是自签名证书需要开启此选项 ] ]; protected $option = [ 'name' => 'official_account_login', 'count' => 1, // 必须是单进程,因为要通过connection的ID来区分客户端连接 'transport' => 'ssl' ]; protected $connections = []; public function __construct() { $this->socket = Config::get('server.official_account_scan_login_server'); $this->context['ssl']['local_cert'] = Env::get('wss.pem'); $this->context['ssl']['local_pk'] = Env::get('wss.key'); parent::__construct(); } public function onWorkerStart(Worker $worker) { $innerTextWorker = new Worker(Config::get('server.inner_http_server')); $innerTextWorker->onMessage = function(TcpConnection $connection, $request) { MyLog::infoLog('http服务接收到的数据', $request, 'workerman'); $loginId = $request['request']['login_id'] ?? ''; if (empty($loginId)) { $connection->send(json_encode(['code' => 1, 'msg' => '缺少login_id'], JSON_UNESCAPED_UNICODE)); return false; } if (isset($this->connections[$loginId])) { $result = json_encode(['code' => 0, 'msg' => '消息推送成功', 'data' => $request['request']], JSON_UNESCAPED_UNICODE); $this->connections[$loginId]->send($result); $connection->send(json_encode(['code' => 0, 'msg' => '接收成功'], JSON_UNESCAPED_UNICODE)); return true; } $connection->send(json_encode(['code' => 0, 'msg' => '发送成功'], JSON_UNESCAPED_UNICODE)); return true; }; $innerTextWorker->listen(); $connections = $this->connections; // 心跳间隔55秒 define('HEARTBEAT_TIME', 55); Timer::add(10, function() use ($connections) { $timeNow = time(); foreach($connections as $connection) { // 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间 if (empty($connection->lastMessageTime)) { $connection->lastMessageTime = $timeNow; continue; } // 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接 if ($timeNow - $connection->lastMessageTime > HEARTBEAT_TIME) { $connection->close(); } } }); } public function onMessage(TcpConnection $connection, $data) { $data = json_decode($data, true); if (isset($data['message']) && $data['message'] == 'heart_check') { $connection->send(json_encode(['code' => 0, 'msg' => '正常连接中', 'data' => ['login_id' => $data['login_id'], 'link_status' => 'online']], JSON_UNESCAPED_UNICODE)); return true; } if (isset($data['response']) && $data['response'] == 'ok' && isset($data['staff_id']) && $data['staff_id'] != '') { InstantMessageLog::updateByWhere(['send_status'=>2,'update_time'=>time()], ['user_staff_id' => $data['staff_id'], 'login_id' => $data['login_id']]); $connection->send(json_encode(['code' => 0, 'msg' => '完成应答', 'data' => ['login_id' => $data['login_id'], 'link_status' => 'online']], JSON_UNESCAPED_UNICODE)); Cache::store('redis')->rm('user_staff_'.$data['staff_id']); return true; } if (!isset($data['login_id'])||empty($data['login_id'])) { $connection->send(json_encode(['code' => 1, 'msg' => 'missing login_id'])); return false; } if (!Cache::store('redis')->get($data['login_id']) && !isset($data['staff_id'])) { $connection->send(json_encode(['code' => 1, 'msg' => 'login_id expired'], JSON_UNESCAPED_UNICODE)); return false; } if (isset($data['staff_id']) && $data['staff_id'] != '') { Cache::store('redis')->set('user_staff_'.$data['staff_id'], $data['login_id']); } $this->connections[$data['login_id']] = $connection; $connection->send(json_encode(['code' => 0, 'msg' => '连接成功', 'data' => ['login_id' => $data['login_id']]], JSON_UNESCAPED_UNICODE)); return true; } public function onConnect($connection) { } }微信扫描/关注事件回调处理 /** * 扫码登录操作 * @param Application $app [description] * @param [type] $message [description] * @return [type] [description] */ public static function handleLogin(Application $app, $message) { $user = $app->user->get($message['FromUserName']); MyLog::infoLog('user', $user, 'wechat'); $oauthUsers = Oauth::where('apiname', 'wechat_official_account') ->where('openid', $user['openid']) ->findOrEmpty() ->toArray(); $content = [ 'error_code' => 0, 'msg' => '未绑定账号', 'type' => 'no_binding', 'oauth_type' => 'wechat_official_account', 'openid' => $user['openid'], 'token' => '', 'login_id' => $message['login_id'] ]; if (!empty($oauthUsers) && $message['staff_id'] != '') { $content['error_code'] = 1; $content['msg'] = '该微信号已绑定其它账号,请更换微信号扫码'; $content['type'] = 'bound_other'; Cache::store('redis')->set($message['login_id'], $content, 120); $result = (new Client())->post(Config::get('server.inner_http_server_url'), [ 'form_params' => $content]); MyLog::infoLog('微信公众号扫码回调信息发送到websocket', $result->getBody()->getContents(), 'wechat'); return '账号绑定失败'; } if (empty($oauthUsers) || empty($oauthUsers['staff_id']) || empty($oauthUsers['user_id'])) { if ($message['staff_id'] != '') { $userStaff = UserStaff::where(['staff_id'=>$message['staff_id'],'is_delete'=>0])->find(); $returnStr = ''; if (!$userStaff) { $content['msg'] = '账号不存在,绑定失败'; $content['type'] = 'binding_fail'; $returnStr = '账号绑定失败'; } else { (new UserService())->bindWechatOfficialAccount(['user_id'=>$userStaff->user_id,'staff_id'=>$userStaff->staff_id,'openid'=>$user['openid']]); $content['msg'] = '账号绑定成功'; $content['type'] = 'bound'; $returnStr = '账号已完成绑定'; } Cache::store('redis')->set($message['login_id'], $content, 120); $result = (new Client())->post(Config::get('server.inner_http_server_url'), [ 'form_params' => $content]); MyLog::infoLog('微信公众号扫码回调信息发送到websocket', $result->getBody()->getContents(), 'wechat'); return $returnStr; } else { Cache::store('redis')->set($message['login_id'], $content, 120); $result = (new Client())->post(Config::get('server.inner_http_server_url'), [ 'form_params' => $content]); MyLog::infoLog('微信公众号扫码回调信息发送到websocket', $result->getBody()->getContents(), 'wechat'); return '感谢您的关注,为了更好的为您提供服务,请绑定已有账号或注册平台账号,只需绑定或注册一次,以后扫码直接登录,且跟您相关的订单进度也会通过公众号推送给您,方便您及时查看。'; } } $field = 'staff_id,user_id,password,salt,staff_name,name,staff_face,department_id,role_id,state,is_company_staff'; $res = UserSv::doUserLogin([], ['staff_id' => $oauthUsers['staff_id']], $field, 'wechat_official_account'); $content['type'] = 'bound'; if(!$res['status']) { $content['error_code'] = 1; $content['msg'] = '登录失败,请联系客服'; Cache::store('redis')->set($message['login_id'], $content, 120); $result = (new Client())->post(Config::get('server.inner_http_server_url'), [ 'form_params' => $content]); MyLog::infoLog('微信公众号扫码回调信息发送到websocket', $result->getBody()->getContents(), 'wechat'); return 'Hi,你来啦~ 已登录成功。'; } $content['msg'] = '登录成功'; $content['token'] = $res['data']['token'] ?? ''; $content['user_info'] = $res['data']; Cache::store('redis')->set($message['login_id'], $content, 120); $result = (new Client())->post(Config::get('server.inner_http_server_url'), [ 'form_params' => $content]); MyLog::infoLog('微信公众号扫码回调信息发送到websocket', $result->getBody()->getContents(), 'wechat'); return 'Hi,你来啦~ 已登录成功。'; }
2024年01月02日
83 阅读
2023-12-25
docker-compose安装kafka
version: '3' services: zookeeper: image: wurstmeister/zookeeper container_name: zookeeper1 ports: - "2181:2181" environment: PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin JAVA_HOME: /usr/lib/jvm/java-7-openjdk-amd64 ZOOKEEPER_VERSION: 3.4.13 ZK_HOME: /opt/zookeeper-3.4.13 kafka: image: wurstmeister/kafka ports: - "8092:8092" volumes: - /www/docker/kafka/logs/kafka-logs:/kafka/kafka-logs - /etc/localtime:/etc/localtime environment: KAFKA_BROKER_ID: 0 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://192.168.113.2:8092 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:8092 PATH: /usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/kafka/bin JAVA_HOME: /usr/local/openjdk-11 JAVA_VERSION: 11.0.15 KAFKA_VERSION: 2.8.1 SCALA_VERSION: 2.13 KAFKA_HOME: /opt/kafka KAFKA_LOG_DIRS: /kafka/kafka-logs LANG: C.UTF-8 container_name: kafka1 tty: true privileged: true stdin_open: true
2023年12月25日
107 阅读
2023-10-17
VIM常用命令
更改文件格式:set ff=formatff有以下几个选项:unix: 使用Unix风格的换行符(n)和UTF-8编码格式。dos: 使用Windows风格的换行符(rn)和UTF-8编码格式。mac: 使用旧式Mac风格的换行符(r)和UTF-8编码格式。unixmac: 使用Unix和Mac兼容的换行符(n或r)和UTF-8编码格式。
2023年10月17日
60 阅读
2023-10-06
基于阿里云OSS开发云盘遇到的问题与解决办法
背景公司为了让文件更具安全性、功能可定制化以及方便其它系统对文件的调用,决定开发一个资料共享平台(云盘),考虑到存储成本、安全性以及带宽费用的问题,采用阿里云对象存储(oss)的方式来上传、保存、打包、下载文件。以下是在开发过程中遇到的问题以及解决办法。问题与解决办法如何加快上传文件的速度一开始的想法是前端先把文件上传到服务器,服务器获取到文件的信息后保存到数据库,同时把文件上传到阿里云oss,这样的话会占用服务器的上下行带宽,服务器带宽的要求很高,而且做了重复性的上传操作,后面采用直传阿里云oss的方式,前端直接把文件上传到阿里云oss,不经过服务器,文件的信息通过回调的方式进行获取,具体可参考阿里云官方文档 服务端签名直传并设置上传回调 。如何保证文件的安全性(防止他人通过链接直接下载到未授权的文件)首先将阿里云oss的bucket读写权限改为私有,然后对文件的地址进行加签并设置访问有效期,下面是加签的例子(PHP):安装阿里云oss官方SDK composer require aliyuncs/oss-sdk-php对文件进行签名 public function getPreviewUrlPathAttr($value, $data) { $ossClient = OssService::getOssClient(); $ossClient->setUseSSL(true); // 使用https进行访问 return $data['source_name'] ? $ossClient->signUrl(Config::get('aliyun_oss.disk_bucket'), $data['source_name'], 3600, 'GET') : ''; }如何获取图片的缩略图,而不是直接展示源文件在文件列表或者预览时需要用到缩略图,一开始直接使用了源文件,这样会导致加载速度变慢,而且会浪费带宽资源,后面发现阿里云oss可以对图片进行裁剪,具体方法是在生成临时访问连接时,传入裁剪参数$options,如下:public function getPreviewUrlPathAttr($value, $data) { $options = [ OssClient::OSS_PROCESS => "image/resize,m_lfit,h_500,w_500" ]; $ossClient = OssService::getOssClient(); $ossClient->setUseSSL(true); return $data['source_name'] ? $ossClient->signUrl(Config::get('aliyun_oss.disk_bucket'), $data['source_name'], 3600, 'GET', $options) : ''; }如何实现批量打包下载文件一开始的想法是先将文件批量下载到服务器,在服务器打包好之后再从服务器下载,这个方法会占用服务器的资源,而且打包速度很慢,不可行。后来发现可以在阿里云oss所在的区域部署云函数进行打包,打包好之后再把文件上传到阿里云oss进行下载,因为在同区域可以使用内网下载以及计算资源是动态分配的,所以打包速度比在公司服务器上打包快好多倍,具体实现参考 使用函数计算zip打包oss文件文件数太大打包时间过长导致浏览器访问断开采用异步的方式解决这个问题,可参考 使用thinkphp、think-queue、redis、supervisor搭建异步任务中心
2023年10月06日
192 阅读
2023-10-04
策略模式在PHP实际开发中的应用
什么是策略模式 策略模式是一种行为型设计模式,它定义了一系列算法,封装每个算法,并使它们可以相互替换。这意味着可以根据不同情况选择不同的算法,而不需要更改使用算法的对象的代码。在策略模式中,有三个主要角色:环境类(Context):它持有一个策略对象的引用,并在需要时调用策略对象的算法。策略接口(Strategy):它定义了所有具体策略类必须实现的方法,它可以是抽象类或接口。具体策略类(ConcreteStrategy):它实现了策略接口,具体实现了策略算法。策略模式的优点是增强了代码的灵活性和可扩展性,可以动态切换算法,减少了各种算法类之间的耦合。同时,策略模式也符合开闭原则,易于理解和维护。策略模式在数据同步中的使用案例背景公司在全国每个省份几乎都有经销商,每个经销商从公司采购产品,公司ERP系统销售出库单审核之后,需要同步到对应的经销商ERP系统的采购入库单,避免让经销商们自己手动录入,提高效率。虽然每个经销商用的都是跟总部一样的ERP系统,但是要同步的字段以及逻辑仍然会有差异,因此需要写一套代码可以根据不同的经销商灵活地切换同步的逻辑。案例说明UML类图首先定义策略接口(Strategy)SyncPurchaseInStockToErpInterface,包含getInstance方法(用于快速实例化策略类)、doSync方法(进行数据同步)、beautifyOrder方法(整理单据字段与格式)、beautifyMaterial方法(整理物料信息的字段与格式)。定义具体策略类Adapter2、Adapter3,对应ID为2和3的经销商,同时实现SyncPurchaseInStockToErpInterface策略接口,补充具体逻辑,因为不同的经销商单据传输的字段和数据有差异,可以在beautifyOrder与beautifyMaterial方法中进行特殊处理。定义环境类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(); } } }
2023年10月04日
107 阅读
2023-10-04
PHP面向对象中的接口在实际工作中的使用场景
什么是PHP面向对象的接口在PHP中,面向对象的接口是用来定义类的行为规范的一种机制,接口可以定义一个类应该实现哪些方法,但不需要具体实现这些方法。使用场景(以单据导出功能为例)UML类图实现原理在日常业务功能开发过程中,单据导出是一个很常用的功能,为了能够顺利的导出数据,主要考虑数据量太大导致内存溢出和响应超时的问题, 因此一般采用异步的方式,虽然每个单据导出的字段和内容不一样,但是可以抽象出共有的地方,异步导出功能共有的地方一般包括开始导出任务、执行导出任务、任务成功回调以及任务失败回调这些方法,这些共有的方法可以用接口来定义,每个单据类可以实现这个接口来补充具体的导出逻辑,然后异步任务根据这个接口依次执行这些方法,如上图所示,定义ExportTaskInterface接口,接口包含startExportTask、doExportTask、exportTaskFailedHandle、taskFinishHandle方法,定义Order类并实现ExportTaskInterface接口,定义ExportExcelJob类对Order实例导出数据相关方法进行调用。实现步骤定义ExportTaskInterface接口<?php namespace app\contract\excel; use think\queue\Job; interface ExportTaskInterface { /** * 开始导出任务 * @param array $params 创建任务的参数 * @return mixed */ public function startExportTask(array $params) : array; /** * 导出Excel * @param array $params * @return mixed */ public function doExportTask(array $params) : array; /** * 任务失败时的处理方法 * @param \Exception $e * @param array $params * @param Job $job * @return mixed */ public function exportTaskFailedHandle(\Exception $e, array $params, Job $job) : array; /** * 任务完成时的处理方法 * @param array $exportResult * @param Job $job * @return mixed */ public function taskFinishHandle(array $exportResult, Job $job) : array; } 定义Order类,并实现ExportTaskInterface接口<?php namespace app\admin\service; use app\admin\model\Region; use app\common\MyLog; use think\Db; use think\facade\Env; use think\facade\Config; use app\admin\model\GoodsBrand as GoodsBrandModel; use app\admin\model\Order; use app\admin\model\OrderDelivery as OrderDeliveryModel; use app\admin\model\OrderGoods; use app\admin\model\OrderPackGoods; use app\admin\model\Products; use app\contract\excel\ExportTaskInterface; use app\facade\model\OrderChange; use app\library\enum\AsyncTaskEnum; use app\library\enum\ErpEnum; use app\models\AsyncTask; use app\models\OrderChangeGoods; use app\models\OrderChangePackGoods; use app\service\common\XlsWriterService; use app\service\QueueService; use app\admin\logic\Erp as ErpLogic; use app\library\exception\{ServiceLogicException}; use app\admin\model\OrderPackGoods as OrderPackGoodsModel; use app\facade\model\OrderGoods as OrderGoodsModelFacade; use app\facade\model\Order as OrderModelFacade; use app\facade\model\Goods as GoodsModelFacade; use app\facade\model\OrderChange as OrderChangeModelFacade; use app\facade\model\UserAccountLog as UserAccountLogModelFacade; use app\facade\model\OrderDelivery as OrderDeliveryModelFacade; use app\facade\model\UserBill as UserBillModelFacade; use app\facade\model\DiyPack as DiyPackModel; use app\facade\model\DiySolution as DiySolutionModel; use app\admin\model\Order as AdminOrderModel; use app\api\model\Order as ApiOrderModel; use app\facade\service\client\ActivityService; use app\plogic\OrderLogic; use excel\Excel; use think\queue\Job; use app\common\BaseResult; use app\common\basic\Base; use app\library\enum\ErrorCodeEnum; class Order implements ExportTaskInterface { /** * 开始导出任务 * @param array $params * @return array */ public function startExportTask(array $params): array { $name = '订货单数据_' . date("YmdHis"); $data = [ 'user_id' => $params['admin_id'], 'name' => $name, 'task_type' => AsyncTaskEnum::TASK_TYPE_EXCEL_EXPORT, 'identification' => AsyncTaskEnum::IDENTIFICATION_EXCEL_EXPORT, 'task_params' => json_encode($params), 'out_file_name' => $name . '.xlsx', ]; $taskId = AsyncTask::createTask($data); $params['task_id'] = $taskId; $params['handler'] = self::class; QueueService::push('excel_export', $params); return ['msg' => '已创建导出任务,请在下载中心查看', 'task_id' => $taskId]; } /** * 进行导出任务 * @param array $params * @return array * @throws \Pxlswrite\DataFormatException */ public function doExportTask(array $params): array { $orderModel=new Order(); $data= $orderModel->get_order($params['where']); if (empty($data)){ throw new ServiceLogicException('没有可导出的数据'); } $now = time(); $filename = "订货单数据{$params['admin_id']}_{$now}.xlsx"; $field = [ 'order_sn' => ['name' =>'订单编号'], 'erp_sn' => ['name' =>'ERP订单编号'], 'user_name' => ['name' =>'客户名称'], 'brand_name' => ['name' =>'品牌'], 'order_status' => ['name' =>'订单状态'], 'pay_status' => ['name' =>'付款状态'], 'pay_name' => ['name' =>'支付方式'], 'is_special_approval' => ['name' =>'是否特殊审批单'], 'prepare' => ['name' =>'是否备货单'], 'goods_amount' => ['name' =>'商品总金额'], 'order_amount' => ['name' =>'订单金额'], 'money_paid' => ['name' =>'已付款金额'], 'cash_money' => ['name' =>'抵扣券抵扣金额'], 'add_time' => ['name' =>'下单时间'], 'pay_time' => ['name' =>'付款时间'], 'address' => ['name' =>'详细地址'], 'tel' => ['name' =>'电话号码'], 'mobile' => ['name' =>'手机号码'], 'consignee' => ['name' =>'收货人姓名'], 'close_reason' => ['name' =>'订单关闭原因'], 'remark' => ['name' =>'订单备注'], 'action_user' => ['name' =>'最后操作人'], 'email' => ['name' =>'邮箱'], 'service_name' => ['name' =>'销售内勤'], 'pay_type' => ['name' =>'结款方式'], ]; $fileObj = XlsWriterService::getInstance() ->constMemory($filename, null, false) ->field($field); $orderStatusFields = ['0'=>'未确认','1'=>'已确认','2'=>'已取消','3'=>'无效','4'=>'退货','5'=>'已分单','6'=>'部分分单','7'=>'已关闭']; $payStatusFields = ['0'=>'未付款','1'=>'付款中','2'=>'已付款','3'=>'部分付款']; $page = 1; while (true){ $fields = ['order_id','is_valet','is_from_quotation','order_total_amount','od.user_id','order_sn','pay_status','money_paid','cash_money','surplus_money','company_name','od.add_time','od.pay_time','order_amount','goods_amount','order_status','shipping_status','action_user','od.remark','od.pay_type','od.consignee','od.tel','od.mobile','pm.pay_name','ui.user_rank','brand_id','order_type','order_from','od.erp_sn','is_special_approval','prepare','od.address','od.close_reason','od.email','ui.service_name','od.integral','od.extension_code','od.is_unite_promotion', 'od.province', 'od.city', 'od.district']; $data= $orderModel->get_order($params['where'],$fields,[],'list',true,$page); $data=$data->toArray(); if (empty($data['data'])){ break; } $orderListData = $data['data']; $order_ids = array_column($orderListData, 'order_id'); $brand_ids = array_column($orderListData, 'brand_id'); $order_goods = OrderService::getInstance()->getOrderGoodsList($order_ids); // 订单商品品牌 $brands = GoodsBrandModel::where('brand_id', 'in', array_unique($brand_ids))->column('brand_id,brand_name'); // 发货单金额 $orderDeliveryAmount = OrderDeliveryModel::where([['order_id','in',$order_ids],['status','in',[0,2,3,4,5]]])->group('order_id')->column('order_id,sum(order_amount)'); $order_change = OrderChange::where(['order_id'=>$order_ids,'status'=>1])->group('order_id')->column('change_id','order_id'); $items = []; foreach ($orderListData as &$value) { $value['pay_name'] = $value['surplus_money'] > 0 ? '余额支付' : $value['pay_name']; // 关闭可退回订单金额 $can_return_amount = $value['money_paid'] == 0 && $value['surplus_money'] > 0 ? $value['surplus_money'] : $value['money_paid']; $delivery_amount = $orderDeliveryAmount[$value['order_id']] ?? 0; if ($delivery_amount > 0) { $can_return_amount = $can_return_amount - $delivery_amount; } if ($value['pay_type'] == 1) { $can_return_amount = 0; } $value['can_return_amount'] = number_format($can_return_amount, 2); $value['brand'] = isset($brands[$value['brand_id']]) ? ['brand_id'=>$value['brand_id'],'brand_name'=>$brands[$value['brand_id']]] : []; if(isset($order_goods[$value['order_id']])) { $value['children'] = $order_goods[$value['order_id']]; foreach ($value['children'] as &$vc) { if ($vc['goods_type'] == 1) { $vc['product_sn'] = $vc['goods_sn']; } } unset($vc); } $value['add_time'] =date('Y-m-d H:i',$value['add_time']); $value['pay_time'] = $value['pay_time'] ? date('Y-m-d H:i',$value['pay_time']) : ''; //是否有变更中的订单 $value['is_changing'] = isset($order_change[$value['order_id']]) ? 1 : 0; $value['operation'] = false; $region = Region::where('region_id', 'in', [$value['province'], $value['city'], $value['district']]) ->column('region_name', 'region_id') ?: []; $province = $region[$value['province']] ?? ''; $city = $region[$value['city']] ?? ''; $district = $region[$value['district']] ?? ''; $value['address'] = $province . $city . $district . $value['address']; } unset($value); $exportData = []; foreach ($orderListData as $key => $value) { $exportData[$key]['order_sn'] = $value['order_sn']; $exportData[$key]['erp_sn'] = $value['erp_sn']; $exportData[$key]['user_name'] = $value['company_name']; $exportData[$key]['brand_name'] = $value['brand']['brand_name'] ?? ''; $exportData[$key]['order_status'] = $orderStatusFields[$value['order_status']]; $exportData[$key]['pay_status'] = $payStatusFields[$value['pay_status']]; $exportData[$key]['pay_name'] = $value['pay_name']; $exportData[$key]['is_special_approval'] = $value['is_special_approval'] ? '是' : '否'; $exportData[$key]['prepare'] = $value['prepare'] ? '是' : '否'; $exportData[$key]['goods_amount'] = $value['goods_amount']; $exportData[$key]['order_amount'] = $value['goods_amount']; $exportData[$key]['money_paid'] = $value['money_paid']; $exportData[$key]['cash_money'] = $value['cash_money']; $exportData[$key]['add_time'] = $value['add_time']; $exportData[$key]['pay_time'] = $value['pay_time']; $exportData[$key]['address'] = $value['address']; $exportData[$key]['tel'] = $value['tel']; $exportData[$key]['mobile'] = $value['mobile']; $exportData[$key]['consignee'] = $value['consignee']; $exportData[$key]['close_reason'] = $value['close_reason']; $exportData[$key]['remark'] = $value['remark']; $exportData[$key]['action_user'] = $value['action_user']; $exportData[$key]['email'] = $value['email']; $exportData[$key]['service_name'] = $value['service_name']; $exportData[$key]['pay_type'] = $value['pay_type'] ? '月结' : '现结'; } foreach ($exportData as $val) { $item = []; foreach ($field as $key => $value) { $item[$key] = $val[$key] ?? ''; } $items[] = array_values($item); } unset($exportData); !empty($items) && $fileObj->data($items); $page++; } $filePath = $fileObj->output(); return ['file_path' => $filePath, 'task_id' => $params['task_id']]; } public function exportTaskFailedHandle(\Exception $e, array $params, Job $job): array { AsyncTask::where('id', $params['task_id'])->update([ 'status' => AsyncTaskEnum::TASK_STATUS_FAIL, 'exception_msg' => $e->getMessage() ]); $job->delete(); return []; } public function taskFinishHandle(array $exportResult, Job $job): array { AsyncTask::where('id', $exportResult['task_id'])->update([ 'status' => AsyncTaskEnum::TASK_STATUS_FINISH, 'download_url' => $exportResult['file_path'] ]); $job->delete(); return []; } }定义ExportExcelJob类<?php namespace app\job\jobs; use app\common\MyLog; use app\contract\excel\ExportTaskInterface; use app\library\exception\ServiceLogicException; use think\queue\Job; class ExportExcelJob { public function fire(Job $job, $data) { if (!isset($data['handler'])){ $job->delete(); throw new ServiceLogicException('缺少处理类'); } try { $this->handleJob(new $data['handler'], $data, $job); }catch (\Exception $e){ var_dump($e->getFile() . $e->getLine() . $e->getMessage()); MyLog::errorLog('error', $e->getFile() . $e->getLine() . $e->getMessage(), 'export_excel_task'); $job->delete(); } } protected function handleJob(ExportTaskInterface $exportTask, $data, Job $job) { MyLog::infoLog('export_excel_task', '接收到Excel导出任务:' . json_encode($data), 'export_excel_task'); try { $result = $exportTask->doExportTask($data); $exportTask->taskFinishHandle($result, $job); MyLog::infoLog('export_excel_task_res', '执行成功:' . json_encode($result), 'export_excel_task'); }catch (\Exception $e){ MyLog::infoLog('export_excel_task_err', $e->getFile() . $e->getLine() . $e->getMessage(), 'export_excel_task'); $exportTask->exportTaskFailedHandle($e, $data, $job); } } public function fail(Job $job, $data) { $job->delete(); } }调用顺序用户点击导出按钮调用Order实例的startExportTask方法,startExportTask方法把当前类以及相关参数传递到ExportExcelJob实例,通过ExportExcelJob实例调用handleJob方法进行导出操作,具体实现逻辑请参考这篇文章 使用thinkphp、think-queue、redis、supervisor搭建异步任务中心 。
2023年10月04日
90 阅读
1
2
...
9