PHP后端开发技术学习 - PHP开发
type: channel
link: https://www.aibasil.cn/category/PHP开发
description: 经验总结
items: NGINX配置静态文件一直报404错误
items: 金蝶ERP与数据中心同步数据方案
items: workerman+websocket实现公众号扫码关注登录
items: docker-compose安装kafka
items: VIM常用命令
items: 基于阿里云OSS开发云盘遇到的问题与解决办法
items: 策略模式在PHP实际开发中的应用
items: PHP面向对象中的接口在实际工作中的使用场景
items: 团队Git分支管理与协作方式（基于分支发布版本）
items: 服务器使用WDCP面板添加网站避坑指南

NGINX配置静态文件一直报404错误
type: item
date: 2024-10-30T16:07:00+08:00
description: 原因一开始配置的路径是错误的，然后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就可以了

金蝶ERP与数据中心同步数据方案
type: item
date: 2024-04-09T11:27:00+08:00
description: 为什么要建设中间件？不能完全去掉金蝶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打触发器不分页获取

workerman+websocket实现公众号扫码关注登录
type: item
date: 2024-01-02T14:41:00+08:00
description: 背景为了方面用户登录系统以及引导用户关注公众号，需要做一个微信扫码登录的功能实现逻辑说明用到的技术或框架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 $work…

docker-compose安装kafka
type: item
date: 2023-12-25T17:17:18+08:00
description: 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

VIM常用命令
type: item
date: 2023-10-17T09:45:52+08:00
description: 更改文件格式: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开发云盘遇到的问题与解决办法
type: item
date: 2023-10-06T09:59:18+08:00
description: 背景公司为了让文件更具安全性、功能可定制化以及方便其它系统对文件的调用，决定开发一个资料共享平台（云盘），考虑到存储成本、安全性以及带宽费用的问题，采用阿里云对象存储（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搭建异步任务中心

策略模式在PHP实际开发中的应用
type: item
date: 2023-10-04T22:08:00+08:00
description: 什么是策略模式 策略模式是一种行为型设计模式，它定义了一系列算法，封装每个算法，并使它们可以相互替换。这意味着可以根据不同情况选择不同的算法，而不需要更改使用算法的对象的代码。在策略模式中，有三个主要角色：环境类（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\Quer…

PHP面向对象中的接口在实际工作中的使用场景
type: item
date: 2023-10-04T16:47:36+08:00
description: 什么是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\a…

团队Git分支管理与协作方式（基于分支发布版本）
type: item
date: 2023-10-03T23:45:55+08:00
description: 前言毕业时进了一家小型项目外包公司，代码版本管理工具用的是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分支后就可以复用公用…

服务器使用WDCP面板添加网站避坑指南
type: item
date: 2023-09-19T09:40:00+08:00
description: 事件场景最近需要给boass添加www.boass.com域名进行直接访问，而不是通过www.boass.com跳转到boass.hopo.com.cn进行访问，添加完成后，达到两个域名都可访问boass的目的。解决的方案是取消www.boass.com解析到boass.hopo.com.cn，而改为解析到120.25.xxx.xxx正式环境服务器，同时下载SSL证书，然后通过服务器的WDCP面板新增www.boass.com域名，上传证书，并且把网站目录指向跟boass.hopo.com.cn一致。操作过程遇到的问题以及解决办法（针对前端项目）问题一：网站添加完成后，访问内容变成了WDCP默认页面的内容原因：新增网站时，WDCP面板会使用默认的index.html文件替换掉已经存在于指定文件夹中的index.html文件，因此会导致开发人员所写的index.html文件被覆盖，访问内容变成了WDCP面板默认的内容。解决办法：新增网站前，如果指定目录中已存在index.html文件，则先备份好，添加好网站后再恢复。或者添加好网站后，再将源代码重新部署到指定文件夹。问题二：新增网站后，导致其它域名配置文件丢失原因：如果当前新增的网站指向的目录与已存在的网站所指向目录一致时，会导致已存在网站的配置文件丢失(域名.conf)，这应该是WDCP面板的问题。解决办法：使用WDCP面板添加网站后，会在/www/wdlinux/nginx/conf/vhost目录生成两个配置文件，一个是conf后缀，另一个wdcp后缀，丢失的是起作用的conf后缀文件，需要基于wdcp后缀的文件重新复制一份与域名名称一致并以conf为后缀的文件（将wdcp后缀文件去掉就是正确的名称），然后重启Nginx服务（service nginxd restart）。问题三：添加网站（或解决配置文件丢失）后，访问域名返回503错误原因：相关网站配置文件location中的try_files指令配置错误，如下location / { try_files $uri @apache; }或location / { try_files $uri $uri/ /?$args; }具体原因可网上搜索Nginx关于try_files指令相关说明，导致了需要手动在域名后输入index.html才能进行访问，不然会返回503错误。解决办法：需要将conf文件中location部分配置成如下内容方可解决问题。location / { try_files $uri $uri/ /index.html; }问题四：将网站删除后重新添加，网站访问异常或者SSL证书不生效原因：这种情况下，WDCP需要重启Nginx服务才能生效解决办法：重启Nginx，目前105服务器重启命令为：service nginxd restart总结使用WDCP添加网站时，如果网站目录指向与已存在网站所指向的目录一致，建议提前备份配置文件；添加前端项目的网站时，需要注意避免index.html入口文件被覆盖的问题；修改相关配置文件后不生效，考虑重启Nginx服务。