常用库.md

  • 压缩图片

/**
 * 图片处理相关
 * composer require intervention/image:2.7.2
 */
class ImageResize
{
    protected $abpath = null; // 传入文件的绝对路径
    protected $config = [];

    /**
     * @var \think\File
     */
    protected $file = null; // 压缩之后的文件
    protected $fileInfo = null;

    public function __construct($publicbasepath)
    {
        $abpath = ROOT_PATH . $publicbasepath;
        if (!file_exists($abpath)) {
            throw new SystemException('文件不存在!');
        }
        $this->abpath = $abpath;
        $this->config = Config::get('upload');

    }
    /**
     * 压缩文件并返回文件信息
     * @param string $publicbasepath public开头的位置
     * @param integer $max_width
     * @param integer $quality
     * @return \think\File
     * @copyright jcleng
     * @author jcleng
     */
    public function resizeimg($max_width = 750, $quality = 90)
    {
        $manager = new ImageManager(['driver' => 'gd']);
        if (!file_exists($this->abpath)) {
            return false;
        }
        $_file = new \think\File($this->abpath);
        // 检查是不是图像
        if (!$_file->checkImg()) {
            return false;
        }
        $image = $manager->make($this->abpath);
        // dump($image->width());
        if ($image->width() <= 740) {
            return false;
        }
        $image->widen($max_width)->encode('jpg', $quality);
        // dump($image->dirname); // /srv/public/uploads/20240103
        // dump($image->filename); // bf5de6af4968df778fca841b9516c710
        // dump($image->extension); // jpg
        // ! 先生成缓存,再覆盖
        $_temp_name = $image->dirname . '/' . 'temp_' . $image->filename . '.' . $image->extension;
        // dump($_temp_name);
        $image->save($_temp_name);
        if (!file_exists($_temp_name)) {
            return false;
        }
        $image_temp = $manager->make($_temp_name);
        if ($image_temp->width() > 0) {
            try {
                rename($_temp_name, $this->abpath);
            } catch (\Throwable $th) {
                throw new SystemException($th->getMessage());
            }
            // 文件信息
            $this->file = new \think\File($this->abpath);
            return true;
        }
        return false;
    }
    // 生成后的文件
    public function file()
    {
        return $this->file;
    }
    // 获取生成文件的文件信息
    public function fileInfo()
    {
        // 是空的
        $fileInfo = $this->file->getInfo();
        // trace('old fileInfo', 'error');
        // trace($fileInfo, 'error');

        $suffix = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
        $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
        $fileInfo['suffix'] = $suffix;
        $fileInfo['imagewidth'] = 0;
        $fileInfo['imageheight'] = 0;
        $fileInfo['filesize'] = filesize($this->abpath);
        $this->fileInfo = $fileInfo;
        $this->checkImage();
        return $this->fileInfo;
    }
    // 获取图片宽度和高度
    protected function checkImage()
    {
        $imgInfo = getimagesize($this->abpath);
        if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) {
            throw new SystemException(__('不是有效的图片文件!'));
        }
        $this->fileInfo['imagewidth'] = isset($imgInfo[0]) ? $imgInfo[0] : 0;
        $this->fileInfo['imageheight'] = isset($imgInfo[1]) ? $imgInfo[1] : 0;
        return true;
    }
}
  • zincsearch 日志

/**
 * ZsLog的日志驱动,批量写入日志到es,在日志配置文件的type使用该类
 * @link https://github.com/zincsearch/zincsearch
 */
class ZsLog
{
    // zs地址
    private static $URL;
    // 密码: base64_encode('admin:123456')
    private static $BASIC_PASSWORD;
    /**
     * 统一写入日志到es
     * ! 这个位置只能放数据, 不允许获取上下文信息, 队列限制
     * @param array $log 多个日志信息
     * @param array $ext_data 附加数据: 用户信息,请求信息,等
     * @return bool
     */
    public function save(array $log, $ext_data)
    {
        $requestid = $ext_data['requestid'];
        $starttime = $ext_data['starttime'];
        $uid = $ext_data['uid'];
        $mobile = $ext_data['mobile'];
        $ip = $ext_data['ip'];
        $endtime = $ext_data['endtime']; // 毫秒
        $sqllog = $ext_data['sqllog'];
        $formattedDateTime = $ext_data['formattedDateTime']; // 当前时间
        $httpstatus = $ext_data['httpstatus'];

        // * 配置
        self::$URL = Env::get('zincsearch.url');
        self::$BASIC_PASSWORD = Env::get('zincsearch.authorization');
        // 请求并写入
        $index_name = self::createIndex();
        $rand_key = time() . '_' . rand(100,200);
        foreach ($log as $value) {
            self::createData([
                'content' => [
                    // 这里使用一个键, 防止字段一致, 类型不一致导致zs报错
                    'info' => $value,
                ],
                'publish_date' => $formattedDateTime,
                // 统一增加 uid
                'uid' => $uid,
                'mobile' => $mobile,
                'ip' => $ip,
                // 初始到日志写入的时间
                'space_time' => $endtime - $starttime,
                'sqllog' => $sqllog,
                'httpstatus' => $httpstatus
            ], $index_name);
        }
        return true;
    }
    /**
     * 创建一个供日志保存的索引
     *
     * @param string $index_name 索引名称
     * @param boolean $if_exist_cache 如果已经创建过了,进行缓存一下,下次不进行创建
     * @return bool
     */
    private static function createIndex($index_name = null, $if_exist_cache = true)
    {
        if (empty($index_name)) {
            // 每个月一个索引
            $index_name = 'logs_' . date('Ym');
        }
        if ($if_exist_cache) {
            // 读取缓存, 如果存在, 不再创建
            $cache_data = Cache::get('zslog_' . $index_name);
            if (!empty($cache_data)) {
                return $cache_data;
            }
        }
        $params = [
            "name" => $index_name,
            "dynamic" => false, // 创建索引时禁用动态映射
            "storage_type" => "disk",
            "mappings" => [
                "properties" => [
                    "content" => [
                        "type" => "text",
                        "index" => true,
                        "store" => true,
                        "highlightable" => true,
                    ],
                    "publish_date" => [
                        "type" => "date",
                        "format" => "2006-01-02 15:04:05",
                        "index" => true,
                        "sortable" => true,
                        "aggregatable" => true,
                    ],
                ],
            ],
        ];
        // ! post请求
        $client = new Client();
        try {
            $client->post(self::$URL . '/api/index', [
                'headers' => [
                    'Content-Type' => 'application/json',
                    'Authorization' => 'Basic ' . self::$BASIC_PASSWORD,
                ],
                'json' => $params,
                'timeout' => QueueLog::STATUS ? 10 : 1,
            ]);
        } catch (GuzzleException $th) {
            if (\think\helper\Str::contains($th->getMessage(), "already exists")) {
                if ($if_exist_cache) {
                    // 缓存
                    Cache::set('zsLog_' . $index_name, 1, 2 * 60);
                }
            }
        }
        return $index_name;
    }
    /**
     * 提交单个日志
     *
     * @param array $data
     * @param string $index_name
     * @return bool
     * @throws \App\Exception\JsonRpcException
     */
    private static function createData($data, $index_name = 'tplog')
    {
        // ! post请求
        $client = new Client();
        try {
            $client->post(self::$URL . '/api/' . $index_name . '/_doc', [
                'headers' => [
                    'Content-Type' => 'application/json',
                    'Authorization' => 'Basic ' . self::$BASIC_PASSWORD,
                ],
                'json' => $data,
                'timeout' => QueueLog::STATUS ? 10 : 1,
            ]);
        } catch (GuzzleException $th) {
            // 写入异常提示
            // echo $th->getMessage();
            // throw $th;
        }
        return true;
    }
}
  • 上下文记录请求数据

/**
 * 上下文, 只能在fpm里面使用;非fpm请增加init()方法每个请求清楚历史数据
 *
 * @copyright jcleng
 * @author jcleng
 */
class Context
{
    private static $data = [];

    /**
     * 获取数据
     *
     * @param string $key
     * @param boolean $when_empty_exception 当数据为空时是否抛出异常
     * @param string $exception_msg 异常提示文本
     * @return mixed
     * @copyright jcleng
     * @author jcleng
     */
    public static function get($key, $when_empty_exception = false, $exception_msg = '数据为空!')
    {
        $data = self::$data[$key] ?? null;
        if (empty($data) && !empty($when_empty_exception)) {
            throw new SystemException($exception_msg);
        }
        return $data;
    }

    public static function set($key, $value)
    {
        self::$data[$key] = $value;
    }

    public static function has($key)
    {
        return array_key_exists($key, self::$data);
    }

    public static function remove($key)
    {
        unset(self::$data[$key]);
    }

    public static function toArray()
    {
        return self::$data;
    }
}

/**
 * 日志上下文
 * 需要在 app_end/app_exception 进行hook操作
 * 通常是在: [应用行为(behavior事件)]请求开始/请求结束返回数据之前/请求当作写日志
    // 请求结束
    'response_end' => [
        'app\\common\\behavior\\Log',
    ],
 * @copyright jcleng
 * @author jcleng
 */
class ContextLog
{
    private static $logs = [];
    private static $requestid = null;
    private static $starttime = null; // 初始化时间, 毫秒
    private static $lasthttpstatus = null; // 最后响应数据的http状态码

    /**
     * 每个请求都需要初始化一次, 通常是基类
     *
     * @return void
     * @copyright jcleng
     * @author jcleng
     */
    public static function init()
    {
        self::$logs = [];
        self::$requestid = date('YmdHis') . '_' . rand(10000, 90000);
        self::$starttime = round(microtime(true) * 1000);
    }
    /**
     * 设置一个日志到logs, 可以重复执行多次, 请求结束之后get()可以拿到所有的数据
     *
     * @param array $value
     * @return void
     * @copyright jcleng
     * @author jcleng
     */
    public static function set($value)
    {
        if (!empty($value['httpstatus'])) {
            self::$lasthttpstatus = $value['httpstatus'];
        }
        self::$logs[] = array_merge([
            'requestid' => self::$requestid
        ], $value);
    }
    /**
     * 获取数据
     *
     * @param string $key
     * @param boolean $when_empty_exception 当数据为空时是否抛出异常
     * @param string $exception_msg 异常提示文本
     * @return mixed
     * @copyright jcleng
     * @author jcleng
     */
    public static function get()
    {
        return self::$logs;
    }
    /**
     * 获取当前请求的requestid
     *
     * @return string
     * @copyright jcleng
     * @author jcleng
     */
    public static function getRequestid() {
        return self::$requestid;
    }
    /**
     * 获取初始化时间,毫秒
     *
     * @return int 毫秒
     * @copyright jcleng
     * @author jcleng
     */
    public static function getStarttime() {
        return self::$starttime;
    }
    /**
     * 获取最后的http状态码
     *
     * @return int http状态码
     * @copyright jcleng
     * @author jcleng
     */
    public static function getLastHttpstatus() {
        return self::$lasthttpstatus;
    }

}

/**
 * sql执行的所有语句放到这个位置
 * ! database.debug 需要设置为开
 * @copyright jcleng
 * @author jcleng
 */
class ContextSql
{
    private static $logs = [];

    /**
     * 每个请求都需要初始化一次, 通常是基类
     * @param boolean $force_record_log 默认情况下这个位置注册listen()之后就[不再写到默认的文件日志]中, 这里打开, 写sql日志到2个位置
     * @return void
     * @copyright jcleng
     * @author jcleng
     */
    public static function init($force_record_log = true)
    {
        self::$logs = [];
        Db::listen(function ($sql, $time, $explain, $master) use ($force_record_log) {
            self::$logs[] = [
                'sql' => $sql,
                'time' => $time,
                'explain' => $explain,
                'master' => $master,
            ];
            if ($force_record_log) {
                // 同时写sql日志到本地文件中
                Log::record('[ SQL ] ' . $sql . ' [ ' . $master . 'RunTime:' . $time . 's ]', 'sql');
                if (!empty($explain)) {
                    Log::record('[ EXPLAIN : ' . var_export($explain, true) . ' ]', 'sql');
                }
            }
        });
    }
    /**
     * 获取sql数据日志列表
     *
     * @param string $key
     * @param boolean $when_empty_exception 当数据为空时是否抛出异常
     * @param string $exception_msg 异常提示文本
     * @return mixed
     * @copyright jcleng
     * @author jcleng
     */
    public static function get()
    {
        return self::$logs;
    }

}
  • orm 更新 json 字段

/**
 * 获取json_set的sql value的值, 最多支持二维数组; 注意json字段默认的值应该为{}否则不能更新
 * ! 查询json字段为{}的值: JSON_UNQUOTE(JSON_EXTRACT(field, '$')) = '{}' 或者 like "%{}%"
 * ! 在orm使用需要 Db::raw()
 * @param string $field 需要更新json的表的字段
 * @param array $jsondata 更新的json键值对
 * @return string
 * @throws \App\Exception\JsonRpcException
 */
function makeJsonSetSqlValue($field, $jsondata = [])
{
    if (empty($jsondata)) {
        return $field;
    }
    $sql = 'JSON_SET(' . $field . '';
    foreach ($jsondata as $key => $value) {
        if (is_array($value)) {
            foreach ($value as $subKey => $subValue) {
                $sql .= ', \'$.' . $key . '.' . $subKey . '\', \'' . $subValue . '\'';
            }
        } else {
            $sql .= ', \'$.' . $key . '\', \'' . $value . '\'';
        }
    }
    $sql .= ')';
    return $sql;
}
  • swoole 导入大批量 csv 文件

/**
 * 导入.csv到 鉴定号&流水号管理
 * 主要使用多协程和yield
 * @author lxx
 */
class ImportCsv
{
    const MAX_BING = 20;

    /**
     * 读取每行的文件
     *
     * @param string $ab_path
     * @return yield
     * @author lxx
     */
    public function readCvs($ab_path)
    {
        $handle = fopen($ab_path, 'r');
        while (feof($handle) === false) {
            yield fgetcsv($handle);
        }
        fclose($handle);
    }
    /**
     * @param string $ab_path
     * @param string $importId 批次id
     */
    public function import($ab_path, $importId)
    {
        $result = $this->readCvs($ab_path);
        $row_space = [];
        $jump = 1000; // 单个数组的最大值
        $i = 0;


        $wg = new \Swoole\Coroutine\WaitGroup();
        $err = 0;
        $errmsg = [];
        try {
            foreach ($result as $key => $row) {
                $i = $i + 1;
                if ($i == 1) {
                    continue;
                }
                if (empty($row[0])) {
                    continue;
                }
                $serial_number = trim($row[0]);
                $identify_code = trim($row[1]);

                // ! 格式化数据
                $sectionList = [];
                if (strpos($identify_code, 'http') !== false) {
                    if (empty($serial_number) || empty($identify_code)) throw new MineException('内容不允许有空值,请仔细检查后重新操作');
                    $band_device_id = 0;
                    $band_device_store_id = 0;
                    if ($i == 1) {
                        $sectionList = container()->get(SupplierDeviceSectionMapper::class)->getModel()->where('status', '=', 1)->get(['id', 'device_id', 'device_store_id', 'section_sta', 'section_end'])->toArray();
                        if (empty($sectionList)) throw new MineException('请进行设置后重新操作');
                    }
                    // 检查数据是否有效
                    foreach ($sectionList as $value) {
                        if ($serial_number_val >= $value['section_sta'] && $serial_number_val <= $value['section_end']) {
                            $band_device_id = $value['device_id'];
                            $band_device_store_id = $value['device_store_id'];
                            break;
                        }
                    }

                    if (empty($band_device_id) || empty($band_device_store_id)) throw new MineException('流水号内容(' . $serial_number_val . ')不在流水号段内,请仔细检查后重新操作');
                } else {
                    // FIXME: 设置数据
                    $band_device_id = 1;
                    $band_device_store_id = 2;
                    $serial_number_val = $serial_number;
                    $identify_code_val = $identify_code;
                }



                // ! 入库数据
                $row_space[] = [
                    'batch_id' => $importId,
                    'serial_number' => $serial_number_val,
                    'report_number' => $identify_code_val,
                    'band_device_id' => $band_device_id,
                    'band_device_store_id' => $band_device_store_id
                ];
                if (count($row_space) % $jump == 0) {
                    // 去使用
                    go(function () use ($wg, &$row_space, &$err, &$errmsg) {
                        $wg->add();
                        try {
                            TableData::insert($row_space);
                        } catch (\Throwable $th) {
                            $err = $err + 1;
                            $errmsg[] = $th->getMessage();
                            throw $th;
                        } finally {
                            $wg->done();
                        }
                    });
                    $row_space = [];
                }
            }
            if (!empty($row_space)) {
                // 去使用
                go(function () use ($wg, &$row_space, &$err, &$errmsg) {
                    $wg->add();
                    try {
                        TableData::insert($row_space);
                    } catch (\Throwable $th) {
                        $err = $err + 1;
                        $errmsg[] = $th->getMessage();
                        throw $th;
                    } finally {
                        $wg->done();
                    }
                });
            }
            $wg->wait();
        } catch (\Throwable $e) {
            while ($wg->count() > 0) {
                \Swoole\Coroutine\System::sleep(1);
            }
            TableData::where('batch_id', $importId)->delete();
            throw new MineException('导入失败,' . $e->getMessage(), 500);
        } finally {
            // 尝试回滚
            if (!empty($err)) {
                // 删除已经入库的数据
                while ($wg->count() > 0) {
                    \Swoole\Coroutine\System::sleep(1);
                }
                TableData::where('batch_id', $importId)->delete();
                throw new MineException('导入失败' . json_encode($errmsg), 500);
            }
        }
        return true;
    }
}
  • 关于批量casewhen更新

<?php

// 通过不同的_order_third_no更新outbound_time
if (!empty($store_process_list_arr)) {
    $sql = 'CASE `merchant_order_no` ';
    foreach ($store_process_list_arr as $id => $item) {
        if (empty($item['operate_time'])) {
            continue;
        }
        $_order_third_no = $item['order_third_no'];
        $_receiving_time = $item['operate_time'];
        $sql .= 'WHEN "' . $_order_third_no . '" THEN ' . "'$_receiving_time' ";
    }
    $sql .= 'ELSE outbound_time END';
    $res = MerchantIdentifyOrder::whereNull('outbound_time')
        ->whereIn('merchant_order_no', $merchant_order_no_s)
        ->update([
            'outbound_time' => Db::raw($sql),
        ]);
}
  • 当group之后需要分组之后最新的一条数据用MAX取一条数据即可

<?php
$log_list = StoreProcessOperate::whereIn('type', ['RETURN_OUTSTORE', 'NORMAL_OUTSTORE'])
    ->whereIn('process_id', $process_id_s)
    ->groupBy('process_id')
    ->orderBy('id', 'desc')
    ->selectRaw("process_id, operate_time, MAX(id) AS max_id")
    ->get()->toArray();
  • 把in条件作为行数据

$ids = ['1', '2', '99993000'];
$result = DB::table(DB::raw("(" . implode(' UNION ALL ', array_map(function ($id) {
    return "SELECT '$id' AS in_id";
}, $ids)) . ") A"))
    ->leftJoin('admin_log B', 'A.in_id', '=', 'B.id')
    ->whereNull('B.id')
    ->selectRaw("A.*")
    ->selectRaw('B.id')
    ->get();
var_export($result);

// 实际sql
SELECT * FROM (
    SELECT '1864186161043653' AS id
    UNION ALL SELECT '1667466847244513'
    UNION ALL SELECT '648250874697543'
    UNION ALL SELECT '2759509227772092'
    UNION ALL SELECT '1340692903883273'
) AS ids
leftJoin product_account on product_account.id = ids.id

// ! 临时表作为join主表进行查询
SELECT * FROM (
    SELECT '1864186161043653' AS id
    UNION ALL SELECT '1667466847244513'
    UNION ALL SELECT '648250874697543'
    UNION ALL SELECT '2759509227772092'
    UNION ALL SELECT '1340692903883273'
)  as temptable
left Join product_account on product_account.id = temptable.id
  • 控制台打印日志

/**
 * 控制台打印trace日志
 *
 * @param mixed $mixdata
 * @return void
 * @author jcleng
 */
function debuglog($mixdata = '')
{
    if (php_sapi_name() === 'cli') {
        fwrite(STDERR, "\n\n");
        fwrite(STDERR, "\033[31m" . '===========DEBUGLOG===========' . "\033[0m\n");
        fwrite(STDERR, "\033[31m" . '==========='.date('Y-m-d H:i:s') . "\033[0m\n");
        fwrite(STDERR, "\033[31m" . json_encode(debug_backtrace(1, 6), 128 + 256) . "\033[0m\n");
        fwrite(STDERR, "\n\n");
    } else {
        error_log("\n\n");
        error_log("\033[31m" . '===========DEBUGLOG===========' . "\033[0m\n");
        error_log("\033[31m" . '==========='.date('Y-m-d H:i:s') . "\033[0m\n");
        error_log("\033[31m" . json_encode(debug_backtrace(1, 6), 128 + 256) . "\033[0m\n");
        error_log("\n\n");
    }
}
  • 使用Vtiful\Excel转换csv为xlsx

use Vtiful\Kernel\Excel;

public function csv2Xlsx($csvPath = '')
{
    // $csvPath = BASE_PATH . "/.vscode/20250806_213328_明细.csv";
    $re_path = BASE_PATH . '/.vscode/';
    $config = ['path' => $re_path]; // 输出目录
    $excel = new Excel($config);

    // 创建 Excel 文件对象
    $filename = pathinfo($csvPath)['filename'] ?? time();
    $file_name = "{$filename}.xlsx";
    $fileObject = $excel->fileName($file_name);

    // 打开 CSV 文件
    $csvFile = fopen($csvPath, "r");

    if ($csvFile === false) {
        die("无法打开 CSV 文件");
    }

    // 读取第一行作为表头
    $headers = fgetcsv($csvFile);
    $fileObject->header($headers); // 写入表头

    // 逐行读取并写入 Excel
    $row_all = [];
    while (($row = fgetcsv($csvFile)) !== false) {
        $row_all[] = $row;
    }
    $fileObject->data($row_all);

    // 关闭 CSV 文件
    fclose($csvFile);

    // 保存 Excel 文件
    $fileObject->output();
    return $re_path . $file_name;
}