原因一开始配置的路径是错误的，然后NGINX配置了静态文件缓存 location ~ .*\.(js|css)?$ { expires 12h; error_log /dev/null; access_log /dev/null; }解决办法 先把缓存注释掉 # location ~ .*\.(js|css)?$ # { # expires 12h; # error_log /dev/null; # access_log /dev/null; # }重新启动NGINX就可以了
在 Linux 系统中，top 是一个用于实时显示系统性能和进程信息的命令。它提供了关于 CPU、内存、任务管理等的详细信息。以下是 top 输出的常见字段及其含义和单位：1. 顶部区域这个区域显示了系统的整体信息。top - 15:30:13 up 2 days, 3:01, 3 users, load average: 0.58, 0.74, 0.6415:30:13：当前系统时间。up 2 days, 3:01：系统已经运行的时间（2 天 3 小时 1 分钟）。3 users：当前登录系统的用户数量。load average: 0.58, 0.74, 0.64：系统在过去 1 分钟、5 分钟和 15 分钟的平均负载（数字越低表示系统越空闲）。2. 任务和进程区域显示当前系统任务和进程的总体情况。Tasks: 123 total, 1 running, 122 sleeping, 0 stopped, 0 zombie123 total：总的任务/进程数量。1 running：正在运行的任务/进程数量。122 sleeping：处于睡眠状态的任务/进程数量。0 stopped：被停止的任务/进程数量。0 zombie：僵尸进程数量。3. CPU 使用情况区域显示 CPU 的使用情况。%Cpu(s): 1.5 us, 0.5 sy, 0.0 ni, 97.8 id, 0.1 wa, 0.0 hi, 0.1 si, 0.0 stus (user)：用户空间占用的 CPU 百分比，不包括 nice 值调整的进程。sy (system)：内核空间占用的 CPU 百分比。ni (nice)：由 nice 值调整优先级的进程占用的 CPU 百分比。id (idle)：空闲的 CPU 百分比。wa (iowait)：等待 I/O 操作的 CPU 百分比。hi (hardware interrupts)：硬件中断占用的 CPU 百分比。si (software interrupts)：软件中断占用的 CPU 百分比。st (steal time)：虚拟机等待实际 CPU 处理的时间百分比。4. 内存使用情况区域显示系统内存和交换空间的使用情况。KiB Mem : 16344964 total, 1234567 free, 9876543 used, 3456789 buff/cachetotal：物理内存总量（单位：KiB）。free：未被使用的空闲内存（单位：KiB）。used：已使用的内存（单位：KiB）。buff/cache：缓存和缓冲区使用的内存（单位：KiB）。KiB Swap: 2097148 total, 2097148 free, 0 used. 5643213 avail Memtotal：交换分区总量（单位：KiB）。free：空闲的交换空间（单位：KiB）。used：已使用的交换空间（单位：KiB）。avail Mem：可用的物理内存（单位：KiB）。5. 进程列表区域显示所有进程的详细信息。每列代表一个不同的属性：PID：进程 ID。USER：进程所有者。PR：优先级。NI：Nice 值，优先级的调整值。VIRT：虚拟内存使用量（单位：KiB）。RES：驻留内存使用量（单位：KiB），即实际在物理内存中使用的部分。SHR：共享内存使用量（单位：KiB）。S：进程状态（S = Sleeping, R = Running, Z = Zombie, D = Uninterruptible Sleep 等）。%CPU：进程使用的 CPU 百分比。%MEM：进程使用的内存百分比。TIME+：进程使用的 CPU 时间总和。COMMAND：启动进程的命令名称或路径。这些字段和指标帮助系统管理员监控系统性能并排查问题。你是否有特定的字段想要更详细的解释？
为什么要建设中间件？不能完全去掉金蝶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打触发器不分页获取
背景为了方面用户登录系统以及引导用户关注公众号，需要做一个微信扫码登录的功能实现逻辑说明用到的技术或框架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，你来啦~ 已登录成功。'; }
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
更改文件格式: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编码格式。
基于阿里云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搭建异步任务中心
什么是策略模式 策略模式是一种行为型设计模式，它定义了一系列算法，封装每个算法，并使它们可以相互替换。这意味着可以根据不同情况选择不同的算法，而不需要更改使用算法的对象的代码。在策略模式中，有三个主要角色：环境类（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(); } } }
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搭建异步任务中心 。
团队Git分支管理与协作方式（基于分支发布版本）
前言毕业时进了一家小型项目外包公司，代码版本管理工具用的是SVN，几个人开发一个项目时，每个人开发完自己的功能后，使用svn commit提交代码到仓库，然后配置的勾子会自动把代码拉到测试环境，测试通过之后，在正式环境的服务器使用svn update命令将代码更新到最新版本。平时下班前会提交代码到仓库，如果遇到某个同事开发进度较慢，要优先发布其它同事完成的功能，就需要将没开发好的功能代码先注释掉，或者等到那个同事开发完再进行发布，那时候也没有深入研究svn有没有更好的办法解决这种问题。在第二家公司用的是GIT，在开发新能时，首先会基于master分支创建一个新版本分支，例如2.7.1分支，用来开发2.7.1版本，然后参与这个版本开发的人员，从这个2.7.1分支再切一个属于自己的分支，例如feature-basil-2.7.1，开发完成后发起合并到2.7.1请求，通过之后拉取代码到测试环境测试，测试通过只有，再把2.7.1合并到master，然后拉取代码到正式环境，那时候只知道按照这样的流程走，没搞清楚为什么要这样搞，后面用的多了，就渐渐明白了一些git的分支管理与多人协作方式。接下来讲的是目前所在公司的Git代码管理与协作方式。基于分支发布的代码管理与协作方式分支说明 主要包括6个分支，常驻分支有production、dev、test、preOnline，临时分支有feature、hotfixproduction生产分支用于生产发布，保存着最新生产环境的代码，不允许在此分支上直接修改，只能通过preOnline分支进行合并preOnline预上线分支用于预上线测试，不允许在此分支上直接修改，只能通过feature或hotfix分支进行合并test测试分支用于测试，不允许在此分支上直接修改，只能通过其它分支进行合并dev开发分支用于开发，不允许在此分支上直接修改，只能通过其它分支进行合并feature功能分支基于production，用于开发新功能hotfix bug修复分支基于production，用于修复bug开发场景工作流说明新功能开发例如A、B两位同事共同开发feature-1.0.0版本，这时候一般会划分为两个模块，一人负责一个模块。首先从production分支切出feature-1.0.0分支，然后A和B分别从feature-1.0.0分支切出自己的本地分支feature-1.0.0-A和feature-1.0.0-B，A、B开发完后将本地feature-1.0.0-A和feature-1.0.0-B合并到feature-1.0.0分支，接着再将feature-1.0.0分支合并到dev分支，合并到dev分支后自动部署到开发环境，在开发环境上进行开发联调，联调完成之后将feature-1.0.0合并到test分支自动部署到测试环境，测试通过之后将feature-1.0.0合并到preOnline分支自动部署到预上线环境，预上线测试完成之后，将preOnline分支合并到production分支自动部署到生产环境，至此，多人进行新版本开发从编写代码到发布上线就完成了。bug修复 如果生产环境出现bug，则从production分支切出hotfix分支，修改完bug之后，将hotfix分支合并到dev分支进行开发调试，调试完成之后将hotfix分支合并到test分支进行测试，测试完成之后将hotfix分支合并到preOnline分支进行预上线测试，预上线测试通过之后将preOnline分支合并到production分支发布到生产环境。答疑为什么使用feature以及hotfix分支合并到test、preOnline分支，而不是将dev合并到test，然后将test合并到preOnline？因为dev分支用于开发联调，可能包含了其它可能版本或者没有开发好的代码，如果使用dev分支合并到test和preOnline就会把其它版本或者没有开发好的代码更新到测试环境或者预上线环境，而使用feature或者hotfix分支单独合并到test、preOnline，则可以单独更新开发好的版本或者模块，就算多个同事开发同一个版本，某个同事开发的模块没有完成，要延后发版，也可以先发其他同事开发好的模块。如果多个同事共同开发一个版本，每个人负责不同的模块，但每个模块有共用的代码怎么办？大家沟通之后，某个同事可以将共用的代码在自己的分支写好，然后再合并到共用分支，然后其他同事再将共用分支合并到自己的分支进行复用，例如上面新功能开发场景的A同事将公用代码在自己的feature-1.0.0-A分支上写好之后，合并到feature-1.0.0分支，B同事将feature-1.0.0分支合并到自己的feature-1.0.0-B分支后就可以复用公用代码了。为什么在开发新功能的时候已经有了feature分支了，每个人还有基于这个feature分支创建一个属于自己的分支？因为每个人负责的模块不一样，而这两个模块的耦合性可能没那么高，可以将某个模块单独发布，这样的话可以对模块进行单独发布，而不必等两个模块开发好了再发布。补充说明这套git代码分支管理方式是基于分支通过Jenkins等工具进行自动部署代码的，dev对应开发环境、test对应测试环境、preOnline对应预上线环境、production对应生产环境。这是根据目前公司的情况所摸索出来的git分支管理与协作方式，可能还有很多需要优化的地方，个人见识有限，欢迎批评指正，提出宝贵的优化建议。