阅读视图

发现新文章,点击刷新页面。
🔲 ☆

Linux 服务器实现 Word 转图片方案

在很多业务系统中,经常需要对 Word 文档进行在线预览或生成图片。

常见场景包括:

  • 电子函件或通知预览
  • 审批系统附件展示
  • OCR 识别前的文档预处理
  • IM / 消息系统模板截图
  • 文档归档与审计

Word 文档 (.docx) 本身不适合直接渲染为图片,因此通常采用如下转换链路:

DOCX → PDF → PNG

原因:

  • DOCX → PDF:排版最稳定
  • PDF → PNG:渲染简单,兼容性好

本文记录一次完整的服务器环境搭建过程,使用:

  • CentOS/PHP
  • phpword:负责处理 Word 模板中的变量替换
  • LibreOffice:负责 Word 转 PDF
  • ImageMagick:负责 PDF 转 PNG

完整转换流程如下:

PHP (phpword)
  ↓
DOCX
  ↓
LibreOffice (soffice)
  ↓
PDF
  ↓
ImageMagick (convert)
  ↓
PNG

LibreOffice

首先在服务器安装 LibreOffice:

sudo yum install libreoffice

安装完成后可以使用 soffice进行文档转换。

测试是否安装成功:

soffice --version

接下来就可以开始 DOCX 转 PDF 了,使用命令:

# 单文件转换
soffice --headless \
--convert-to pdf \
/tmp/upload_data/letter.docx \
--outdir /tmp/upload_data

执行后会在/tmp/upload_data目录下生成letter.pdf

如果目录中有大量 Word 文件,也可以批量转换:

soffice --headless \
--convert-to pdf \
/tmp/upload_data/1/*.docx \
--outdir /tmp/upload_data/1

执行后会在/tmp/upload_data/1/目录下生成和 docx 文件同名的 pdf 文件。

但是这个时候查看生成好的 PDF 文件,会发现字体和 Word 有区别,在 Linux 服务器上转换 Word 时,经常会遇到:

  • 字体被替换
  • 排版错位
  • 中文显示异常

原因是服务器没有 Windows 上对应的字体。解决方法就是从 Windows 复制字体,放到服务器上。

字体目录:

C:\Windows\Fonts

常用中文字体:

字体 文件
宋体 simsun.ttc
黑体 simhei.ttf
微软雅黑 msyh.ttc
仿宋 simfang.ttf
楷体 simkai.ttf

将这些字体打包为fonts.zip后上传服务器,上传到服务器后执行:

unzip fonts.zip -d /usr/share/fonts/chinese

刷新字体缓存:

fc-cache -fv

查看字体:

fc-list | grep simsun

如果看到输出,说明字体安装成功。

然后重启 LibreOffice:

systemctl restart libreoffice

这样 LibreOffice 才会加载新字体,可以正常生成 PDF 文件了。

ImageMagick

转为 PDF 以后,就可以使用 ImageMagick 将 PDF 渲染为图片。

安装:

yum install -y ImageMagick ImageMagick-devel

检查版本:

convert -version

基本转换命令:

convert -density 300 letter.pdf letter.png

参数说明:

参数 说明
-density 300 设置 PDF 渲染 DPI
letter.pdf 输入文件
letter.png 输出文件

默认情况下,每页 PDF 都会生成一张图片,多页 PDF 时就会生成letter-0.pngletter-1.png...

如果需要拼接多页为一张图片,生成一张长图:

convert -density 300 letter.pdf -append letter.png

-append表示纵向拼接,+append表示横向拼接。

同时可以加上-background white -alpha remove,防止透明背景。

完整示例命令:

soffice --headless --convert-to pdf letter.docx
convert -density 300 letter.pdf -background white -alpha remove -append letter.png

通过 LibreOffice 与 ImageMagick,可以在 Linux 服务器上实现稳定的文档转换流程。

🔲 ☆

2025|在代码之外,我学会了旅行、牵手和拥抱

2025 年,对我来说不是被某一个高光时刻定义的一年,而是一段在持续构建中慢慢成型的过程。

白天,我仍然在研发世界里与需求和 Bug 周旋,用一次次迭代换取稳定与确定;而当屏幕合上,生活开始显影:我们去旅行,把时间交给路途;牵手、拥抱,在忙碌与疲惫之间确认彼此的存在。

技术在向前,生活也在向前,而这一年最重要的关键词,或许正是这些看似柔软、却足够支撑长期前行的瞬间:旅行、牵手、拥抱,以及永远的我们。

工作:自由度、责任感,以及真正属于自己的事

去年换了新的工作,到了 2025 年,这种变化开始真正显现出来。

事情不再只是“完成任务”,而是被当作一份长期经营的事业来对待。工作时间更可控,选择空间也更大,节奏上不再是持续高压的忙碌,而是一种忙中有闲、但心里有数的状态。

在工作之外,我依旧活跃在开源世界中,5 月,我在 opensource.org 发布了我的开源故事:Lu Fei: Open Source Changed My Life – From User to Maintainer,因为有了更多可支配的时间,也重新提起了 docsify 的维护工作,今年发布了两个 RC 版本,目标是在 2026 年正式发布 5.0。

回头看这一年,技术没有离开中心,但它不再是消耗型的存在,而是逐渐成为支撑自由度、事业感和长期价值的底层能力。

生活:从同行开始,走向“我们”

如果说 2025 年的技术关键词是稳定与自由,那生活这条线,更像是一段被时间慢慢推着向前的故事。

一个很有意思的 callback 是:

去年,我和一位关系很好的同事一起离职;

今年,她成了我的女朋友。

这是蓄谋已久,还是命运绕了一圈?现在回头看,答案其实已经不重要了。

这一年,我们总是在路上,所有看似的错位,最后都成了走向彼此的路线。

3 月,她的一句“期待 3.14 我们青岛见”,我没去成,我是在月底之前才回去的,见了见以前的同事,聊了聊近况,吃喝玩乐一圈;

4 月,我去宁波出差,她去了亚庇;

5 月,她去郑州看 KPL 比赛,我在金鸡湖边喝咖啡。

那段时间,好像总差一步,却始终没有走远。

5 月中旬,我去了北京找她。一个人逛了国博、人民大会堂,那是第一次意识到:有些地方,一个人去是“参观”,两个人去才是“记忆”。

6 月,杭州出差,我去了灵隐寺、西湖、天下第一财神庙,替她请了一个手串,还特地坐了 1314 路公交车;

7 月,我第一次不想剪头发,想试试不一样的自己,染了她帮我选的颜色——树莓红;

8 月,秋天的第一杯奶茶,是霸王茶姬,送给我天下第一好的朋友。后来回头看,那大概已经不是一杯奶茶那么简单了;

9 月,我在南京,她在杭州,又一次错过。南京博物院、古鸡鸣寺、夫子庙,一个人走完,灵隐寺能斩孽缘,鸡鸣寺能斩乱桃花,哦~

直到 9 月底,她换了新工作,我们终于在杭州重合了行程。

一起爬天下第一财神庙,这次索道刚好检修,她一路等我,我爬得半死;路上还有位姐姐助攻:现在怎么还有小年轻出来爬山约会的。她没有反驳~

一起在西溪湿地坐了摇橹船,我们面对面坐着,船行得很慢,橹声一下一下落在水面上。视线会不经意地对上,又很快移开,听她说了一些工作上的槽点。

那一刻我其实很想问点什么,但话到嘴边,又咽了回去。很多话还没说出口,但距离,已经不是原来的样子了。

10 月,国庆,我们各自回到生活里。

我没回家,有天晚上出门去看了烟花,发给了她;她在我生日那天送了礼物,买了花和蛋糕。

国庆后错峰出行,我又去找她了。可能是看完《盛夏芬德拉》,终于学会了“长嘴”,我问她:

“你觉得我们还算是普通朋友吗?”

答案如我所愿。

那之后,北京的故事正式展开:

古代建筑博物馆、天坛公园、红螺寺、雁栖湖,爬山、骑车;

她看着我累得不行,笑得特别开心;

还有北京野生动物园,下车后往后勾勾的小手~

热恋期的我们,几乎每天都要打三四个小时的视频。

11 月,一起去了北京欢乐谷过万圣节,拍了那张用来给家里官宣的照片;

8 号一起去鸟巢看了 KPL 年度总决赛;

某个深夜,她因为太想我,买了一束花送到了我公司。

那一刻,想念终于有了形状。

11 月底,搬了新家,一起看了《疯狂动物城》。

“Love you, partner”

她第二次给我写了手写信,上一次,还是我们一起离职的时候。

12 月,我出差长沙,给她写了明信片;

看到剧里女主叠小星星写日记,我给她叠了 520 颗;

她带我去看了芭蕾舞剧《海盗》,居然没有睡着,原来我也可以这么“优雅”;

圣诞节的 callback,去年圣诞节在上海的匆匆一面,到今年的相拥而眠。

回看这一年,我们走过很多城市,也在不同节奏里慢慢靠近。

关系并不是某一个瞬间被确认的,而是在一次次同行中自然发生。

至于之后的路,不急着写完,

我们继续走下去就好。

🔲 ☆

使用 GitHub Actions 自动同步 Docker 镜像到 CNB

为了提升访问速度、增强稳定性并规避部分官方源的不确定性,将常用的开源镜像同步到中国大陆可访问的镜像仓库是一种高效的解决方案。

本文介绍如何通过 GitHub Actions 自动化完成该同步流程,支持选择性构建与定制版本。

背景

许多开源镜像托管在 Docker Hub 上,但由于网络、访问频率限制等问题,拉取速度不稳定,甚至存在连接失败的情况。

之前是使用 GitHub Actions 同步到 CODING 上,不过要 CODING 要停服了,所以改为同步到 CNB 了。

cnb.cool 也是由腾讯出品,基于 Docker 生态,对环境、缓存、插件进行抽象,通过声明式的语法,帮助开发者以更酷的方式构建软件。

支持代码托管、云原生构建和云原生开发等功能。

同步方案概览

  • 镜像列表维护在 .github/images.yml 中;
  • 手动或自动触发 GitHub Actions;
  • 使用 skopeo 工具将镜像从 Docker Hub 同步到 CNB;
  • 支持选择同步特定镜像或全部镜像;
  • 支持指定版本(tag)。

GitHub Actions 工作流详解


点击查看完整文件内容

前往 GitHub 查看:docker-proxy.yml

name: Mirror Docker Images to CNB

on:
  workflow_dispatch:
    inputs:
      name:
        description: 'Select image to mirror (or leave blank to mirror all)'
        required: false
        type: choice
        options:
          - ""
          - vaultwarden
          - bark-server
          - elasticsearch
          - mysql
          - hyperf
          - clickhouse
      version:
        description: 'Override tag version'
        required: false
        type: string
  push:
    paths:
      - '.github/images.yml'
    branches: [ 'main' ]

jobs:
  mirror:
    name: >-
      Mirror ${{ github.event.inputs.name || 'All Images' }}${{ github.event.inputs.version && format(' (version: {0})', github.event.inputs.version) || '' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Mirror images
        env:
          INPUT_NAME: ${{ github.event.inputs.name }}
          INPUT_VERSION: ${{ github.event.inputs.version }}
        run: |
          images=$(yq -o json '.images' ${{ github.workspace }}/.github/images.yml)

          if [ -n "$INPUT_NAME" ]; then
          matrix=$(echo "$images" | jq -c --arg name "$INPUT_NAME" '.[] | select(.name == $name)')
          else
          matrix=$(echo "$images" | jq -c '.[]')
          fi

          if [ -z "$matrix" ]; then
          echo "No matching images found for name: $INPUT_NAME"
          exit 1
          fi

          echo "$matrix" | while read -r item; do
            image=$(echo "$item" | jq -r '.image')
            name=$(echo "$item" | jq -r '.name')
            default_tag=$(echo "$item" | jq -r '.tag')
            tag=${INPUT_VERSION:-$default_tag}

            echo "Mirroring $image:$tag to docker.cnb.cool/lufei/docker/$name:$tag"

            skopeo copy --all docker://docker.io/${image}:${tag} \
              docker://docker.cnb.cool/lufei/docker/${name}:${tag} \
              --src-creds "${{ secrets.DOCKERHUB_USERNAME }}:${{ secrets.DOCKERHUB_TOKEN }}" \
              --dest-creds "cnb:${{ secrets.CNB_DOCKER_TOKEN }}"

            echo "::notice title=Image Published::https://docker.cnb.cool/lufei/docker/${name}:${tag}"
          done

触发机制

on:
  workflow_dispatch:
    inputs:
      name:     # 可选镜像名
      version:  # 可选覆盖 tag
  push:
    paths:
      - '.github/images.yml'
    branches: [ 'main' ]

支持两种触发方式:

  1. 手动触发:可选择特定镜像名和版本;
  2. 自动触发:当 .github/images.yml 文件变更时,自动同步全量更新。

镜像清单文件

.github/images.yml

images:
  - image: "vaultwarden/server"
    tag: "latest"
    name: "vaultwarden"
  - image: "finab/bark-server"
    tag: "latest"
    name: "bark-server"
  ...

核心同步逻辑

skopeo copy --all docker://docker.io/${image}:${tag} \
  docker://docker.cnb.cool/lufei/docker/${name}:${tag} \
  --src-creds "${{ secrets.DOCKERHUB_USERNAME }}:${{ secrets.DOCKERHUB_TOKEN }}" \
  --dest-creds "cnb:${{ secrets.CNB_DOCKER_TOKEN }}"
  • --all:确保同步多平台镜像(如 amd64 和 arm64);
  • 使用 GitHub Secrets 配置凭据;
  • 输出同步完成后可直接拉取的地址提示。

如何使用

1. 手动触发

在 GitHub → Actions → “Mirror Docker Images to CNB” → 点击 “Run workflow”:

选择镜像

  • 留空镜像名 → 同步所有;
  • 指定镜像名 → 同步该镜像;
  • 同时指定 tag → 覆盖默认 tag。

指定tag

2. 自动触发

每次更新 .github/images.yml 文件并推送到 main 分支时,会自动同步所有的镜像。

镜像拉取示例

同步完成后,用户可直接通过 CNB 公网地址拉取镜像:

docker pull docker.cnb.cool/lufei/docker/hyperf:8.3-alpine-v3.21-swoole

加上https://可以访问网页查看详情:

输出可以访问的镜像链接

安全配置(GitHub Secrets)

名称 用途说明
DOCKERHUB_USERNAME 用于拉取源镜像
DOCKERHUB_TOKEN Docker Hub 登录 token
CNB_DOCKER_TOKEN CNB 镜像仓库推送凭据

CNB 默认可以创建 npm、Composer 等的制品库,但 Docker 的制品默认就在仓库中,所以创建一个仓库即可。

总结

通过 GitHub Actions + skopeo + CNB 服务,我们构建了一个可复用、自动化、支持多镜像同步的工具链,显著提升了镜像的可用性与部署效率。

🔲 ☆

怎么申请开具中国税收居民身份证明?

之前因为 Google Adsense 要求进行新加坡税务信息填写,需要上传税务证明,但是实在找不到个人在哪里申请,于是就暂停了 Google Adsense 服务。

不过在 2025 年 4 月 1 日起施行了新规,申请《中国税收居民身份证明》不再困难了!

下面就来说说申请步骤:

  1. 访问自然人电子税务局官网,找到我要办税,点击中国税收居民身份证明开具

自然人电子税务局官网

  1. 点击申请开具《中国税收居民身份证明》。后续查询也是这里,点击旁边的查询。

申请开具《中国税收居民身份证明》

  1. 选择申请年度等信息,选择完成后下一步会要求你确认主管税务机关,这个是根据任职受雇单位自动匹配,只需要选择使用那个单位申报就可以。

选择申请年度

  1. 填写申请信息,这里的要求比较多,我下文直接提供了

填写申请信息

  • 对方纳税人名称:Google Asia Pacific Pte. Ltd.
  • 拟适用协定名称:中华人民共和国政府和新加坡共和国关于对所得避免双重征税和防止偷漏税的协定
  • 拟适用协定条款:独立个人劳务条款
  • 拟享受协定待遇收入金额(元):按实际填,比如 720
  • 预计减免税金额(元):按实际填,比如 72
  • 相关附件:使用 Google AdSense 的在线服务条款即可,从这里下载:AdSense 在线服务条款
  • 申请人信息会自动补全
  • 在中国境内是否有住所:是
  • 有住所个人提供因户籍、家庭、经济利益关系而在中国境内习惯性居住的证明材料或说明材料:多选,可以选户籍或者经济利益关系,选择后需要上传身份证、户口簿,我还补充了自己公司的营业执照

在中国境内是否有住所填写

  • 证明领取方式:电子领取

点击下一步提交确认等待主管机关审核即可。

审核通过后没有通知,需要自己去登录网站去查看,如果有问题的话,可能会被拒绝,或者主管机关会给你打电话。

目前我已经通过了,1 号政策开放的时候提交了一次,前几天主管机关下级给我打电话说没有下发成功,让我撤销重新提交一次,这次提交后两三天就通过了。

🔲 ☆

HMAC 签名编码的坑:Go 和 PHP 的不同处理方式

在开发过程中,我们经常使用 HMAC(散列消息认证码)对数据进行签名,以确保数据完整性和身份验证。

然而,不同编程语言在对签名数据进行编码时可能会有所不同,导致相同的 HMAC 计算在不同语言中产生不同的结果。

这篇文章也是因为我直接将 PHP 的签名算法扔给 ChatGPT 生成,并没有实际测试,导致客户反馈签名计算失败,测试后才发现的。

本文将以 Go 和 PHP 为例,探讨为什么直接对 HMAC 签名进行 Base64 编码与先转换为 16 进制字符串再编码的结果不同。

代码示例

Go 代码

package main

import (
    "crypto/hmac"
    "crypto/sha1"
    "encoding/base64"
    "encoding/hex"
    "fmt"
)

func main() {
    data := "hello"
    password := "123456"
    h := hmac.New(sha1.New, []byte(password))
    h.Write([]byte(data))
    signatureBytes := h.Sum(nil)

    // 直接对 HMAC 结果进行 Base64 编码
    base64Signature := base64.StdEncoding.EncodeToString(signatureBytes)
    fmt.Println(base64Signature) // 输出:NYSQUfYBHG0EZ6pU+r+Iw4CvPIQ=

    // 先转换成 16 进制字符串,再进行 Base64 编码
    hexString := hex.EncodeToString(signatureBytes)
    base64OfHex := base64.StdEncoding.EncodeToString([]byte(hexString))
    fmt.Println(base64OfHex) // 输出:MzU4NDkwNTFmNjAxMWM2ZDA0NjdhYTU0ZmFiZjg4YzM4MGFmM2M4NA==
}

PHP 代码

<?php
$data = "hello";
$password = "123456";

// 直接对 HMAC 结果进行 Base64 编码
echo base64_encode(hash_hmac('sha1', $data, $password, true));
// 输出:NYSQUfYBHG0EZ6pU+r+Iw4CvPIQ=

echo "\n";

// 先转换成 16 进制字符串,再进行 Base64 编码
echo base64_encode(hash_hmac('sha1', $data, $password));
// 输出:MzU4NDkwNTFmNjAxMWM2ZDA0NjdhYTU0ZmFiZjg4YzM4MGFmM2M4NA==
?>

为什么结果不同?

表面上看,Go 和 PHP 代码的逻辑是相同的,但它们的 Base64 结果却不同。

其根本原因在于编码前的输入数据不同。

PHP 参数定义文档

hash_hmac(
    string $algo,
    string $data,
    #[\SensitiveParameter] string $key,
    bool $binary = false
): string

PHP 手册中也提到了:当 binary 设置为 true 输出原始二进制数据,设置为 false 输出小写 16 进制字符串。

原始二进制 vs. 16 进制字符串

  1. 原始二进制数据
    • 在 Go 代码中,signatureBytes 是 HMAC 计算出的二进制数据。
    • 在 PHP 代码中,hash_hmac('sha1', $data, $password, true) 也返回二进制数据。
    • 直接对这些二进制数据进行 Base64 编码,输出的是编码后的 HMAC 结果。
  2. 16 进制字符串转换
    • 在 PHP 中,hash_hmac('sha1', $data, $password) 默认返回 16 进制字符串,每个字节被转换成 2 个字符。
    • 在 Go 中,hex.EncodeToString(signatureBytes) 也会将二进制数据转换为 16 进制字符串。
    • 由于 16 进制字符串的长度是原始二进制数据的 2 倍,在进行 Base64 编码时,最终结果也会完全不同。

Base64 编码的作用

Base64 编码的主要作用是将二进制数据转换为文本格式,便于在 URL 或 JSON 等环境中传输。

它不会改变数据的内容,而是按照固定的方式将每 3 个字节转换为 4 个可打印字符。

因此,输入数据的不同会直接影响最终的编码结果。

如何保证一致性?

如果希望跨语言 HMAC 计算保持一致,建议:

  • 确保 Base64 编码前的数据格式一致,统一使用二进制数据进行编码。
  • 在 PHP 中,使用 hash_hmac('sha1', $data, $password, true) 以获取二进制结果。
  • 在 Go 中,直接使用 base64.StdEncoding.EncodeToString(signatureBytes),避免中间转换为 16 进制字符串。

结论

  • 直接对 HMAC 结果进行 Base64 编码,能保持原始数据格式,保证数据可还原。
  • 先转换为 16 进制字符串再进行 Base64 编码,会导致数据翻倍,最终的编码结果不同。
  • 在不同语言间使用 HMAC 签名时,务必保证编码方式的一致性,以避免验证失败。

希望这篇文章能帮助你理解 HMAC 签名在不同语言中的编码差异,并在开发中避免类似的问题!

🔲 ☆

Bitwarden Secrets Manager:简化 DevOps 的机密管理

在 DevOps 和开发流程中,如何安全高效地管理机密数据(如密码、API 密钥和认证信息)是一个重要话题。

Bitwarden 是一款开源密码管理工具,帮助用户存储、管理并共享敏感信息。Bitwarden 推出了新产品Secrets Manager,专为 DevOps 团队和开发人员提供简化的机密管理方案。

对于使用 GitHub Actions 等 CI/CD 工具的团队来说,Secrets 是一种存储机密信息的常见方式。

但是,GitHub Actions 中的 Secrets 一旦保存,就无法查看或修改,这使得本地保存机密变得繁琐且易出错。

而在团队环境中,个人和公司电脑之间的同步问题更是增加了额外的复杂性。

Bitwarden Secrets Manager 解决了这一难题,提供了安全、高效的机密存储与管理方式。

免费额度和在线服务

与传统的 Bitwarden 密码管理器类似,Secrets Manager 也支持 self-hosting,但需要授权才能进行。

为了简化流程(白嫖),也可以使用 Bitwarden 提供的在线服务,享受如下免费额度:

  • 无限量的 Secrets
  • 2 位用户
  • 3 个项目
  • 3 个机器账户

这些免费额度对于大多数小型或中型团队来说已足够使用。

如何集成 GitHub Actions?

Bitwarden Secrets Manager 便于与 GitHub Actions 等 CI/CD 服务进行集成。

以下是一个简单的示例,展示了如何在 GitHub Actions 中获取并使用存储在 Bitwarden 中的机密。

首先,在 GitHub Actions 的工作流 YAML 文件中,添加获取机密的步骤:

- name: Get Secrets
  uses: bitwarden/sm-action@v2
  with:
    access_token: ${{ secrets.BW_ACCESS_TOKEN }}
    base_url: https://vault.bitwarden.com
    secrets: |
      fc3a93f4-2a16-445b-b0c4-aeaf0102f0ff > SECRET_NAME_1
      bdbb16bc-0b9b-472e-99fa-af4101309076 > SECRET_NAME_2

在上面的示例中,fc3a93f4-2a16-445b-b0c4-aeaf0102f0ffbdbb16bc-0b9b-472e-99fa-af4101309076 是存储在 Bitwarden Secrets Manager 中的机密 ID,而 SECRET_NAME_1SECRET_NAME_2 是引用机密的名称,用于在后续步骤中进行使用。

接着,在后续步骤中,使用这些机密值:

- name: Use Secret
  run: SQLCMD -S MYSQLSERVER -U "$SECRET_NAME_1" -P "$SECRET_NAME_2"

完整示例:

- name: Get Secrets
  uses: bitwarden/sm-action@v2
  with:
    access_token: ${{ secrets.BW_ACCESS_TOKEN }}
    secrets: |
      fc3a93f4-2a16-445b-b0c4-aeaf0102f0ff > GITHUB_GPG_PRIVATE_KEY
      bdbb16bc-0b9b-472e-99fa-af4101309076 > GITHUB_GPG_PRIVATE_KEY_PASSPHRASE

- name: Import GPG key
  uses: crazy-max/ghaction-import-gpg@v6
  with:
    gpg_private_key: ${{ env.GITHUB_GPG_PRIVATE_KEY }}
    passphrase: ${{ env.GITHUB_GPG_PRIVATE_KEY_PASSPHRASE }}
    git_user_signingkey: true
    git_commit_gpgsign: true

更多集成方式请参考 官方文档

Secrets Manager CLI 使用

为了便于在本地查询和管理机密,Bitwarden 提供了强大的 Secrets Manager CLI 工具。可以通过它来创建、删除、编辑和列出机密。

GitHub Releases 下载适合操作系统的可执行文件,运行以下命令以查看帮助信息:

bws --help

使用 Secrets Manager CLI 时,需先配置访问令牌(Access Token),然后运行如下命令列出机密:

# 设置环境变量
export BWS_ACCESS_TOKEN=xxxxxx
bws secret list

# 或者从命令行传入
bws secret list --access-token xxxxxx

Alfred Workflow 集成

为了进一步提高效率,写了一个小工具,帮助在 macOS 上快速查询并复制 Secrets。

Alfred Workflow 效果

以下是完整的 PHP 脚本示例:

<?php

$tokenName = !empty($argv[1]) ? trim($argv[1]) : '';
$accessToken = getenv('BWS_ACCESS_TOKEN');
$iconPngUrl = 'icon.png';

$json = `bws secret list -t '{$accessToken}'`;
if (!$json) {
    echo json_encode(['items' => [['title' => 'Error: Failed to fetch secrets', 'valid' => false]]]);
    exit;
}

$list = json_decode($json, true);

$items = [];

foreach ($list as $item) {
    if (!empty($tokenName) && stripos($item['key'], $tokenName) === false) {
        continue;
    }

    $items[] = [
        'arg' => $item['value'],
        'title' => $item['key'],
        'subtitle' => $item['note'],
        'icon' => ['path' => $iconPngUrl],
        'valid' => true,
    ];
}

if (empty($items)) {
    $items[] = [
        'title' => 'No secrets found',
        'valid' => false
    ];
}

echo json_encode(['items' => $items]);
exit;

Bitwarden Secrets Manager 为 DevOps 团队提供了一种更加安全、便捷的方式来管理和集成机密信息。

无论是与 GitHub Actions 集成,还是使用 CLI 工具进行本地管理,Bitwarden 都提供了简洁而强大的功能,帮助提升工作效率并确保敏感数据的安全。

🔲 ☆

MySQL 字符集与大小写敏感性解析

在 MySQL 数据库中,UTF-8 及其变体是最常用的字符集。

不同的 UTF-8 编码可能对大小写敏感性产生影响,主要包括以下几种:

  • utf8:MySQL 早期的 UTF-8 实现,最多支持 3 字节,无法存储部分 Emoji 字符。
  • utf8mb4:MySQL 5.5+ 版本推荐使用的 UTF-8 编码,最多支持 4 字节,能够完整存储所有 Unicode 字符。

字符集与排序规则(Collation)

MySQL 字符集搭配不同的排序规则(Collation)可能会影响查询的大小写敏感性。

常见的排序规则包括:

  • utf8_general_ci / utf8mb4_general_ci:不区分大小写(Case Insensitive,ci 代表 Case Insensitive)。
  • utf8_bin / utf8mb4_bin:区分大小写(Binary,bin 代表按二进制存储,严格区分大小写)。
  • utf8_unicode_ci / utf8mb4_unicode_ci:更符合 Unicode 规范的排序方式,不区分大小写。

默认情况下,utf8_general_ciutf8mb4_general_ci 在搜索时是不区分大小写的。

MySQL 大小写搜索问题

当 MySQL 表的字符集设置为 utf8_general_ciutf8mb4_general_ci 时,使用 LIKE= 进行查询时,默认是不区分大小写的。

例如:

SELECT * FROM users WHERE username = 'admin';

如果数据库中存储了 AdminADMIN 等,查询会返回这些所有匹配项。

如果需要执行区分大小写的查询,则需要:

  1. 修改排序规则(Collation)
ALTER TABLE users MODIFY username VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

这样查询就会严格区分 adminAdmin

  1. 使用 BINARY 关键字
SELECT * FROM users WHERE BINARY username = 'admin';

这样 admin 只会匹配完全相同的字符串,而不会匹配 AdminADMIN 等。

在 ThinkPHP 框架中使用 whereRaw 进行原生查询

在 ThinkPHP 框架中,默认的 where 方法不支持直接使用 BINARY 进行查询,但可以通过 whereRaw 方法执行 MySQL 原生查询。

$result = Db::table('users')
    ->whereRaw("BINARY username = ?", ['admin'])
    ->find();

$result = Db::table('users')
    ->whereRaw("BINARY username LIKE ?", ['%admin%'])
    ->select();

这种方法可以避免默认的大小写不敏感查询,让 MySQL 进行更严格的匹配。

总结

  • MySQL 的 utf8_general_ciutf8mb4_general_ci 默认不区分大小写。
  • 需要区分大小写时,可以修改排序规则(Collation)或使用 BINARY 关键字。
  • 在 ThinkPHP 框架中,可以使用 whereRaw 方法执行 MySQL 原生查询,确保大小写敏感匹配。

这样,你就可以在 ThinkPHP 框架中更灵活地处理 MySQL 字符集大小写敏感的问题。

🔲 ☆

PHP 中生成带毫秒的时间戳

今天在对接一个 API 的时候,发现需要生成高精度的时间戳,格式为yyyyMMddHH24mmssSSS

本文将介绍两种常见的实现方式,并讨论它们的优缺点。

时间格式解析

格式 yyyyMMddHH24mmssSSS 的含义如下:

  • yyyy:四位数的年份(例如:2025)。
  • MM:两位数的月份(01-12)。
  • dd:两位数的日期(01-31)。
  • HH24:两位数的小时(24 小时制,00-23)。
  • mm:两位数的分钟(00-59)。
  • ss:两位数的秒(00-59)。
  • SSS:三位数的毫秒(000-999)。

例如,时间 2025-01-02 11:30:45.123 的格式化结果为:20250102113045123

使用 DateTime 类实现

以下是使用 DateTime 类生成毫秒时间戳的代码示例:

<?php
$dateTime = new DateTime();
// 获取当前时间的微秒数并计算为毫秒
$milliseconds = intval($dateTime->format('u') / 1000);
// 格式化时间
$formattedTime = $dateTime->format("YmdHi") . $dateTime->format("s") . sprintf("%03d", $milliseconds);
echo $formattedTime;

代码解析

  1. $dateTime->format('u') 返回当前时间的微秒(6 位数,例如 123456)。
  2. intval($dateTime->format('u') / 1000) 将微秒转换为毫秒(3 位数,例如 123)。
  3. 使用 sprintf("%03d", $milliseconds) 确保毫秒部分始终为 3 位数(不足时补零)。

示例输出

假设当前时间为 2025-01-02 11:30:45.123456,输出结果为:

20250102113045123

使用 microtime 函数实现

另一种方法是结合 microtime()date() 函数:

<?php
$microtime = microtime(true);
// 格式化时间到秒
$formattedDate = date('YmdHis', floor($microtime));
// 获取毫秒部分
$milliseconds = sprintf('%03d', ($microtime - floor($microtime)) * 1000);
// 拼接毫秒
$formattedDate .= $milliseconds;
echo $formattedDate;

代码解析

  1. microtime(true) 返回当前 Unix 时间戳,包含秒和小数部分。
  2. floor($microtime) 获取整数秒部分。
  3. ($microtime - floor($microtime)) * 1000 提取小数部分并转换为毫秒。
  4. 最终拼接秒部分和毫秒部分,生成完整的时间戳。

示例输出

假设当前时间为 2025-01-02 15:30:45.123456,输出结果为:

20250102153045123

对比分析

特性 使用 DateTime 使用 microtime()
代码简洁性 更加现代化,语义清晰 较为传统,需要手动处理毫秒
精度 取决于系统支持的时间精度 依赖 microtime() 的实现
扩展性 更容易与其他 DateTime 操作结合 适合处理与 Unix 时间戳相关的逻辑

统一验证输出

为了验证两种方法的输出是否一致,可以添加以下代码:

if ($formattedTime === $formattedDate) {
    echo "两种方法的输出一致:$formattedTime\n";
} else {
    echo "两种方法的输出不一致:\n第一种方法:$formattedTime\n第二种方法:$formattedDate\n";
}

总结

  • 如果你的项目主要使用 DateTime 类,建议采用第一种方法,代码语义更加清晰。
  • 如果需要与 microtime() 或 Unix 时间戳直接交互,可以选择第二种方法。

选择哪种方式主要取决于项目需求和代码风格偏好。希望本文对你在生成带毫秒的时间戳方面有所帮助!

🔲 ☆

坚韧与成长的2024年

2024 年,对于我而言,是一段充满挑战与成长的旅程。

从工作中的挫折到家庭的变故,这一年让我更加体会到坚韧的意义,也在风雨中积累了成长的力量。

虽然充满波折,但我仍然选择在逆境中寻找方向,努力让自己变得更加强大。

挑战:逆境的洗礼

在 2024 年中生活与工作的双重挑战交织而来,让我猝不及防。

工作的波折与解脱

其实在去年的年终总结中也提到了,自己已经内耗很久了,而且大部分同事其实也是,职业道路面临着严峻考验,加上大环境不好,工作和家庭多方面带来的压力并不小。

内耗了大概有快一年吧,今年最终也是爆发了:我和一位关系较好的同事一起离职了。

主要原因也是多方面,一是内耗这么久了,不想耗了,世界这么大我想去看看;二是自己的直性格和新同事的背刺;三是公司内部种种事件的发生,使得原本热爱的工作逐渐成为负担。

内耗严重,消耗了我的热情与精力。经过一段时间的挣扎,我决定离开这个让我不再快乐的环境。

离职后的日子可能并不轻松,但也让我重新掌握了生活的主动权,离职后也有听到有几位关系好的同事也先后离开了公司,活成了吃瓜群众...

生活的波澜与坚守

因为房租还未到期,自己也选择了继续在青岛待上一段时间,跟关系好的同事们陆续告别,吃饭喝酒,侃侃而谈,大家也在聊着各自的看法和感受。

离职后,尝试让自己暂时放下压力,为未来的职业方向重新规划,也有疑问为什么不等到过年后再离职?答案还是上面提到的不想耗了,世界这么大我想去看看。

于是我也答应了跟我同一天离职的同事,帮她送兔子去北京,顺便溜达散心,从北京回来之后再起程回老家。

在北京我俩也是喝酒畅聊,还去了一趟雍和宫,在上班和上进之间选择了上香。时间仓促,第三天早上我便出发返程了,不料半路下雨,在出服务区时过了一个小水坑,结果车出现了问题(后来检查得知是电子转向机由于进水坏了),没办法只能一路坚持从高速开回了住处,找了地方维修车辆,避免耽误回老家。

由于转向机突然损坏,不得不承担一笔不小的维修费用,这让本不富裕的家庭雪上加霜,虽然看似是一次小意外,却让我深刻意识到,人生的计划往往会被突如其来的问题打断,我们能做的,是从容面对。

青岛的短暂停留给了我片刻喘息,与老同事的聚会让我感受到人情的温暖。然而,在房租到期后回到老家,奶奶的去世又一次让我面对人生的无常,同时也第一次在医院过夜。

这段时间,我更多地陪伴家人,感悟到亲情的重要性,也开始学会放慢脚步。

就在我努力整理心情,接受上海新工作的同时,命运再次掀起波澜:父亲突然离世,很急很急,我只能在视频中去医院之前见了最后一眼,没想到隔了不到几个小时就得到了人已经走了的消息。

这一噩耗让我深陷悲痛,当晚就请假第二天乘坐最早的飞机回家,奶奶去世时还有父亲在前面撑着,而父亲突然走了,让我手足无措,但我知道我不能垮掉,还有更多的事情在等着我处理。

这个家的生活还要继续,处理完父亲的身后事,直到三七结束,我也重返工作岗位。这段经历不仅考验了我的承受能力,也让我更加清楚地理解了责任的真正含义。

坚韧:在风雨中积累力量

生活和工作的波折让我意识到,坚韧不仅仅是一种态度,更是一种能力。

面对工作中的失落,我学会了取舍,明白了有时候“止损”是对自己最好的成全;面对生活中的离别,我选择直面悲伤,在亲情的回忆中汲取前行的力量。

这一年,尽管前路坎坷,但我始终没有停下脚步。

在适应新的工作节奏中,我不断调整状态,力求用最好的表现迎接挑战。生活的种种考验让我学会了平衡,懂得了如何在压力中找到一丝安慰,让自己变得更坚强、更从容。

成长:收获与前行

2024 年的经历让我深刻体会到,成长往往藏在不经意的每一个瞬间。

离职后的三个月是我重新审视自己的机会,除了本质工作以外,开源事业也是我发自内心热爱的方向。

Docsify 成为 Gitee 成为 GVP 的那一刻,我感受到努力被认可的喜悦,也让我更加坚定地投入到技术与开源社区中。

此外,成为 Apache Answer 的 PPMC Member 也是今年的一大收获。

这份荣誉不仅是一种认可,更是一份责任,促使我不断提升自己,为开源社区贡献更多价值。

无论是在工作还是生活中,我都在这些起起伏伏中找到了成长的契机。

从经历挫折到收获信心,这一年的每一步都让我离更好的自己更近了一些。

展望:迈向崭新的明天

回顾 2024 年,我经历了从失望到解脱,从低谷到重生的全过程。

这些经历让我更加明白,生活并不会因为我们的疲惫而停下脚步,而我们能做的就是在每一次风雨中找到支撑自己的力量。

展望未来,我希望自己能更加沉着应对工作中的挑战,也会投入更多精力到开源社区中,用技术赋能更多人,在社区活动中认识更多人;在生活中,我会更加珍惜与家人、朋友的每一刻,用心感受生活带来的温暖与感动。

这一年让我更加坚信,无论面对什么,只要心中有光,终能走出阴霾。

2025 年,希望是一个充满希望和新机遇的年份,我期待着更多的可能性,也期待着与成长同行,继续前行。

🔲 ☆

2025|在代码之外,我学会了旅行、牵手和拥抱

2025 年,对我来说不是被某一个高光时刻定义的一年,而是一段在持续构建中慢慢成型的过程。

白天,我仍然在研发世界里与需求和 Bug 周旋,用一次次迭代换取稳定与确定;而当屏幕合上,生活开始显影:我们去旅行,把时间交给路途;牵手、拥抱,在忙碌与疲惫之间确认彼此的存在。

技术在向前,生活也在向前,而这一年最重要的关键词,或许正是这些看似柔软、却足够支撑长期前行的瞬间:旅行、牵手、拥抱,以及永远的我们。

工作:自由度、责任感,以及真正属于自己的事

去年换了新的工作,到了 2025 年,这种变化开始真正显现出来。

事情不再只是“完成任务”,而是被当作一份长期经营的事业来对待。工作时间更可控,选择空间也更大,节奏上不再是持续高压的忙碌,而是一种忙中有闲、但心里有数的状态。

在工作之外,我依旧活跃在开源世界中,5 月,我在 opensource.org 发布了我的开源故事:Lu Fei: Open Source Changed My Life – From User to Maintainer,因为有了更多可支配的时间,也重新提起了 docsify 的维护工作,今年发布了两个 RC 版本,目标是在 2026 年正式发布 5.0。

回头看这一年,技术没有离开中心,但它不再是消耗型的存在,而是逐渐成为支撑自由度、事业感和长期价值的底层能力。

生活:从同行开始,走向“我们”

如果说 2025 年的技术关键词是稳定与自由,那生活这条线,更像是一段被时间慢慢推着向前的故事。

一个很有意思的 callback 是:

去年,我和一位关系很好的同事一起离职;

今年,她成了我的女朋友。

这是蓄谋已久,还是命运绕了一圈?现在回头看,答案其实已经不重要了。

这一年,我们总是在路上,所有看似的错位,最后都成了走向彼此的路线。

3 月,她的一句“期待 3.14 我们青岛见”,我没去成,我是在月底之前才回去的,见了见以前的同事,聊了聊近况,吃喝玩乐一圈;

4 月,我去宁波出差,她去了亚庇;

5 月,她去郑州看 KPL 比赛,我在金鸡湖边喝咖啡。

那段时间,好像总差一步,却始终没有走远。

5 月中旬,我去了北京找她。一个人逛了国博、人民大会堂,那是第一次意识到:有些地方,一个人去是“参观”,两个人去才是“记忆”。

6 月,杭州出差,我去了灵隐寺、西湖、天下第一财神庙,替她请了一个手串,还特地坐了 1314 路公交车;

7 月,我第一次不想剪头发,想试试不一样的自己,染了她帮我选的颜色——树莓红;

8 月,秋天的第一杯奶茶,是霸王茶姬,送给我天下第一好的朋友。后来回头看,那大概已经不是一杯奶茶那么简单了;

9 月,我在南京,她在杭州,又一次错过。南京博物院、古鸡鸣寺、夫子庙,一个人走完,灵隐寺能斩孽缘,鸡鸣寺能斩乱桃花,哦~

直到 9 月底,她换了新工作,我们终于在杭州重合了行程。

一起爬天下第一财神庙,这次索道刚好检修,她一路等我,我爬得半死;路上还有位姐姐助攻:现在怎么还有小年轻出来爬山约会的。她没有反驳~

一起在西溪湿地坐了摇橹船,我们面对面坐着,船行得很慢,橹声一下一下落在水面上。视线会不经意地对上,又很快移开,听她说了一些工作上的槽点。

那一刻我其实很想问点什么,但话到嘴边,又咽了回去。很多话还没说出口,但距离,已经不是原来的样子了。

10 月,国庆,我们各自回到生活里。

我没回家,有天晚上出门去看了烟花,发给了她;她在我生日那天送了礼物,买了花和蛋糕。

国庆后错峰出行,我又去找她了。可能是看完《盛夏芬德拉》,终于学会了“长嘴”,我问她:

“你觉得我们还算是普通朋友吗?”

答案如我所愿。

那之后,北京的故事正式展开:

古代建筑博物馆、天坛公园、红螺寺、雁栖湖,爬山、骑车;

她看着我累得不行,笑得特别开心;

还有北京野生动物园,下车后往后勾勾的小手~

热恋期的我们,几乎每天都要打三四个小时的视频。

11 月,一起去了北京欢乐谷过万圣节,拍了那张用来给家里官宣的照片;

8 号一起去鸟巢看了 KPL 年度总决赛;

某个深夜,她因为太想我,买了一束花送到了我公司。

那一刻,想念终于有了形状。

11 月底,搬了新家,一起看了《疯狂动物城》。

“Love you, partner”

她第二次给我写了手写信,上一次,还是我们一起离职的时候。

12 月,我出差长沙,给她写了明信片;

看到剧里女主叠小星星写日记,我给她叠了 520 颗;

她带我去看了芭蕾舞剧《海盗》,居然没有睡着,原来我也可以这么“优雅”;

圣诞节的 callback,去年圣诞节在上海的匆匆一面,到今年的相拥而眠。

回看这一年,我们走过很多城市,也在不同节奏里慢慢靠近。

关系并不是某一个瞬间被确认的,而是在一次次同行中自然发生。

至于之后的路,不急着写完,

我们继续走下去就好。

🔲 ☆

使用 GitHub Actions 自动同步 Docker 镜像到 CNB

为了提升访问速度、增强稳定性并规避部分官方源的不确定性,将常用的开源镜像同步到中国大陆可访问的镜像仓库是一种高效的解决方案。

本文介绍如何通过 GitHub Actions 自动化完成该同步流程,支持选择性构建与定制版本。

背景

许多开源镜像托管在 Docker Hub 上,但由于网络、访问频率限制等问题,拉取速度不稳定,甚至存在连接失败的情况。

之前是使用 GitHub Actions 同步到 CODING 上,不过要 CODING 要停服了,所以改为同步到 CNB 了。

cnb.cool 也是由腾讯出品,基于 Docker 生态,对环境、缓存、插件进行抽象,通过声明式的语法,帮助开发者以更酷的方式构建软件。

支持代码托管、云原生构建和云原生开发等功能。

同步方案概览

  • 镜像列表维护在 .github/images.yml 中;
  • 手动或自动触发 GitHub Actions;
  • 使用 skopeo 工具将镜像从 Docker Hub 同步到 CNB;
  • 支持选择同步特定镜像或全部镜像;
  • 支持指定版本(tag)。

GitHub Actions 工作流详解


点击查看完整文件内容

前往 GitHub 查看:docker-proxy.yml

name: Mirror Docker Images to CNB

on:
  workflow_dispatch:
    inputs:
      name:
        description: 'Select image to mirror (or leave blank to mirror all)'
        required: false
        type: choice
        options:
          - ""
          - vaultwarden
          - bark-server
          - elasticsearch
          - mysql
          - hyperf
          - clickhouse
      version:
        description: 'Override tag version'
        required: false
        type: string
  push:
    paths:
      - '.github/images.yml'
    branches: [ 'main' ]

jobs:
  mirror:
    name: >-
      Mirror ${{ github.event.inputs.name || 'All Images' }}${{ github.event.inputs.version && format(' (version: {0})', github.event.inputs.version) || '' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Mirror images
        env:
          INPUT_NAME: ${{ github.event.inputs.name }}
          INPUT_VERSION: ${{ github.event.inputs.version }}
        run: |
          images=$(yq -o json '.images' ${{ github.workspace }}/.github/images.yml)

          if [ -n "$INPUT_NAME" ]; then
          matrix=$(echo "$images" | jq -c --arg name "$INPUT_NAME" '.[] | select(.name == $name)')
          else
          matrix=$(echo "$images" | jq -c '.[]')
          fi

          if [ -z "$matrix" ]; then
          echo "No matching images found for name: $INPUT_NAME"
          exit 1
          fi

          echo "$matrix" | while read -r item; do
            image=$(echo "$item" | jq -r '.image')
            name=$(echo "$item" | jq -r '.name')
            default_tag=$(echo "$item" | jq -r '.tag')
            tag=${INPUT_VERSION:-$default_tag}

            echo "Mirroring $image:$tag to docker.cnb.cool/lufei/docker/$name:$tag"

            skopeo copy --all docker://docker.io/${image}:${tag} \
              docker://docker.cnb.cool/lufei/docker/${name}:${tag} \
              --src-creds "${{ secrets.DOCKERHUB_USERNAME }}:${{ secrets.DOCKERHUB_TOKEN }}" \
              --dest-creds "cnb:${{ secrets.CNB_DOCKER_TOKEN }}"

            echo "::notice title=Image Published::https://docker.cnb.cool/lufei/docker/${name}:${tag}"
          done

触发机制

on:
  workflow_dispatch:
    inputs:
      name:     # 可选镜像名
      version:  # 可选覆盖 tag
  push:
    paths:
      - '.github/images.yml'
    branches: [ 'main' ]

支持两种触发方式:

  1. 手动触发:可选择特定镜像名和版本;
  2. 自动触发:当 .github/images.yml 文件变更时,自动同步全量更新。

镜像清单文件

.github/images.yml

images:
  - image: "vaultwarden/server"
    tag: "latest"
    name: "vaultwarden"
  - image: "finab/bark-server"
    tag: "latest"
    name: "bark-server"
  ...

核心同步逻辑

skopeo copy --all docker://docker.io/${image}:${tag} \
  docker://docker.cnb.cool/lufei/docker/${name}:${tag} \
  --src-creds "${{ secrets.DOCKERHUB_USERNAME }}:${{ secrets.DOCKERHUB_TOKEN }}" \
  --dest-creds "cnb:${{ secrets.CNB_DOCKER_TOKEN }}"
  • --all:确保同步多平台镜像(如 amd64 和 arm64);
  • 使用 GitHub Secrets 配置凭据;
  • 输出同步完成后可直接拉取的地址提示。

如何使用

1. 手动触发

在 GitHub → Actions → “Mirror Docker Images to CNB” → 点击 “Run workflow”:

选择镜像

  • 留空镜像名 → 同步所有;
  • 指定镜像名 → 同步该镜像;
  • 同时指定 tag → 覆盖默认 tag。

指定tag

2. 自动触发

每次更新 .github/images.yml 文件并推送到 main 分支时,会自动同步所有的镜像。

镜像拉取示例

同步完成后,用户可直接通过 CNB 公网地址拉取镜像:

docker pull docker.cnb.cool/lufei/docker/hyperf:8.3-alpine-v3.21-swoole

加上https://可以访问网页查看详情:

输出可以访问的镜像链接

安全配置(GitHub Secrets)

名称 用途说明
DOCKERHUB_USERNAME 用于拉取源镜像
DOCKERHUB_TOKEN Docker Hub 登录 token
CNB_DOCKER_TOKEN CNB 镜像仓库推送凭据

CNB 默认可以创建 npm、Composer 等的制品库,但 Docker 的制品默认就在仓库中,所以创建一个仓库即可。

总结

通过 GitHub Actions + skopeo + CNB 服务,我们构建了一个可复用、自动化、支持多镜像同步的工具链,显著提升了镜像的可用性与部署效率。

🔲 ☆

PHP 8.3 将于 2023 年 11 月 23 日发布

每年年底,PHP 项目都会发布新的 PHP 主要或次要版本。截至本文发布时,PHP 8.3 已经发布了 RC6 版本,按照发布计划,正式版将于 11 月 23 日发布。

支持版本

除了庆祝新的版本发布以后,也需要注意一下:PHP 8.0 的生命周期即将结束,PHP 8.0 早已在2022 年 11 月 26 日结束了积极支持,而安全支持也将在 PHP8.3 发布的三天后2023 年 11 月 26 日停止。

了解更多信息可查看Supported Versions

新特性

PHP 8.3 引入了许多新功能。然而,它的功能比 PHP 8.1 或 PHP 8.2 相对较少。

PHP 8.3 的主要新特性:

  • 类型化类常量
  • 动态类常量获取
  • #[\Override]属性
  • 只读修改
  • 添加json_validate函数
  • 添加Randomizer::getBytesFromString()方法
  • 添加Randomizer::getFloat()Randomizer::nextFloat()方法

类型化类常量

现在可以在定义常量时,增加类型。

// PHP < 8.3
interface I {
    // We may naively assume that the PHP constant is always a string
    const PHP = 'PHP 8.2';
}

class Foo implements I {
    const PHP = []; // But it may be an array...
}

// PHP 8.3
interface I {
    const string PHP = 'PHP 8.3';
}

class Foo implements I {
    const string PHP = [];
}

// Fatal error: Cannot use array as value for class constant Foo::PHP of type string

动态类常量获取

在之前的版本中获取类的常量,除了直接调用以外,想要动态获取只能通过拼接后使用constant来实现,而现在可以直接使用变量来获取常量。

这个方式在枚举类型中也可以使用。

// PHP < 8.3
class Foo {
    const PHP = 'PHP 8.2';
}

$searchableConstant = 'PHP';

var_dump(constant(Foo::class . "::{$searchableConstant}"));

// PHP 8.3
class Foo {
    const PHP = 'PHP 8.3';
}

$searchableConstant = 'PHP';

var_dump(Foo::{$searchableConstant});

[# 添加#](/?s= 添加)[\Override]`属性

通过给方法添加 #[\Override] 属性,PHP 将确保在父类或实现的接口中存在同名的方法。

添加该属性可以清楚地表明重载父类方法是有意为之,并简化了重构过程,因为重载父类方法的删除会被检测到。

// PHP < 8.3
use PHPUnit\Framework\TestCase;

final class MyTest extends TestCase
{
    protected $logFile;

    protected function setUp(): void
    {
        $this->logFile = fopen('/tmp/logfile', 'w');
    }

    protected function taerDown(): void
    {
        fclose($this->logFile);
        unlink('/tmp/logfile');
    }
}

// The log file will never be removed, because the
// method name was mistyped (taerDown vs tearDown).

// PHP 8.3
use PHPUnit\Framework\TestCase;

final class MyTest extends TestCase
{
    protected $logFile;

    protected function setUp(): void
    {
        $this->logFile = fopen('/tmp/logfile', 'w');
    }

    #[\Override]
    protected function taerDown(): void
    {
        fclose($this->logFile);
        unlink('/tmp/logfile');
    }
}

// Fatal error: MyTest::taerDown() has #[\Override] attribute,
// but no matching parent method exists

只读修改

只读属性现在可以在魔术方法 __clone 方法中修改一次,以实现只读属性的深度克隆。

// PHP < 8.3
readonly class Foo {
    public \DateTime $dateTime;

    function __construct(\DateTime $dateTime) {
        $this->dateTime = $dateTime;
    }

    public function __clone()
    {
        $this->dateTime = clone $this->dateTime;
    }
}

$today = new Foo(new \DateTime());
$tomorrow = clone $today;

// Fatal error: Cannot modify readonly property Foo::$dateTime

// PHP 8.3
readonly class Foo {
    public \DateTime $dateTime;

    function __construct(\DateTime $dateTime) {
        $this->dateTime = $dateTime;
    }

    public function __clone()
    {
        $this->dateTime = clone $this->dateTime;
    }
}

$today = new Foo(new \DateTime());
$tomorrow = clone $today;

$tomorrow->dateTime->modify('+1 day');

添加json_validate函数

在之前的版本中想要验证一个字符是否是语法上有效的JSON,需要先decode然后判断错误码,而现在可以直接调用json_validate函数。

同时 json_validate() 性能比 json_decode() 要好不少,并且使用更加简单。

// PHP < 8.3
function json_validate(string $string): bool {
    json_decode($string);
    return json_last_error() === JSON_ERROR_NONE;
}

var_dump(json_validate('{ "test": { "foo": "bar" } }')); // true

// PHP 8.3
var_dump(json_validate('{ "test": { "foo": "bar" } }')); // true

一次 Lint 多个文件

PHP CLI 二进制文件的 -l 允许检查 PHP 文件以确保它没有语法错误。

以前只允许一次检查一个文件,这意味着如果想检查整个项目,则必须为每个应用程序文件调用一次它。从 PHP 8.3 开始允许传递多个文件。

// PHP < 8.3
php -l index.php

// PHP 8.3
php -l src/**/*.php

除此之外,还有一些新的类、接口和函数,以及弃用和向后兼容性中断。

具体的内容可以期待 PHP 8.3 发布后查看官方的发布公告和文档。

🔲 ☆

通过微信开放平台授权获取公众号文章

如果想要实现一个将公众号文章搬运到某个社区中去发布的功能,除了使用爬虫等方法,还可以通过微信开放平台授权,调用官方接口来获取公众号文章。

类似于腾讯云开发者社区自媒体分享计划、OSCHINA 开源中国 OSC 源创计划,这两个平台都有搬家的功能。

开源中国应该百分百使用了此接口,而腾讯云开发者社区就不一定了,通过查看公众号设置中的授权管理,腾讯云官网只授权了公众号账号信息服务获取认证状态信息两个权限集。

而开源中国的授权权限就是本文中要说明的两个:公众号账号信息服务素材管理

  • 公众号账号信息服务 用于获取授权公众号的信息,例如头像、名称、二维码地址、头像地址、简介等
  • 素材管理 用于获取发布的文章等

配置

开始之前需要先注册微信开放平台,有一个发布过文章的公众号用于测试。

在微信开发平台中进行配置:

微信第三方平台详情

  1. 获取APPIDAppSecret
  2. 开发配置中配置公众号权限集:3 公众号账号信息服务11 素材管理
  3. 开发配置中配置开发资料,设置授权流程相关的域名和白名单等。

开发资料

开发资料填写示例如下:

  • 授权事件接收配置:example.com/events.php,用于接收平台推送给第三方平台账号的消息与事件,如授权事件通知、component_verify_ticket 等。
  • 消息与事件接收配置:example.com/$APPID$/events.php。本文中的场景应该用不到。
  • 授权发起页域名:example.com,必须从本域名内网页跳转到登录授权页,才可完成登录授权
  • 其他的就按提示说明进行填写配置。

测试使用

使用 PHP 语言为例,ngrok 进行内网穿透。

使用 easywechat 的 SDK:

composer require w7corp/easywechat:^6.7

新建4个文件,实际开发中应结合所使用的框架进行封装。

  1. auth.php 用于生成微信授权的链接,这里需要生成链接可点击,因为微信的授权链接需要携带referer才正常授权。不然可能会遇到错误提示:请确认授权入口页所在域名,与授权后回调页所在域名相同,并且,此两者都必须与申请第三方平台时填写的授权发起页域名相同
  2. events.php 用于接收授权事件;
  3. callback.php 授权后的回调地址;
  4. test.php 用来编写获取公众号账号信息、文章的代码。

首先初始化 SDK,这部分代码在4个文件中都要添加,下文会省略。

include __DIR__ . '/vendor/autoload.php';

use EasyWeChat\OpenPlatform\Application;

$config = [
    'app_id' => '', // 开放平台账号的 appid
    'secret' => '', // 开放平台账号的 secret
    'token' => '', // 开发资料中配置的消息校验Token
    'aes_key' => '', // 开发资料中配置的消息加解密Key
];

$app = new Application($config);

events.php

SDK 默认会处理事件 component_verify_ticket ,并会缓存 verify_ticket。暂时不需要处理其他事件,所以直接这样使用:

include __DIR__ . '/vendor/autoload.php';

use EasyWeChat\OpenPlatform\Application;

$config = [
    'app_id' => '', // 开放平台账号的 appid
    'secret' => '', // 开放平台账号的 secret
    'token' => '', // 开发资料中配置的消息校验Token
    'aes_key' => '', // 开发资料中配置的消息加解密Key
];

$app = new Application($config);

$server = $app->getServer();

return $server->serve();

auth.php

$app = new Application($config);
# 使用对应的回调地址,授权后会跳转到该地址
$url = $app->createPreAuthorizationUrl('https://example.com/callback.php');

# 需要通过点击携带referer
echo "<a href='{$url}'>{$url}</a>";

点击生成后的链接,扫码进行授权,扫码完成并授权后,页面将跳转到callback.php

callback.php

$app = new Application($config);
$authorization = $app->getAuthorization($_GET['auth_code']);
$authorizationInfo = $authorization->toArray()['authorization_info'];

var_dump($authorizationInfo);

$authorizerAppid = $authorizationInfo['authorizer_appid'];
$authorizerAccessToken = $authorizationInfo['authorizer_access_token'];
$authorizerRefreshToken = $authorizationInfo['authorizer_refresh_token'];

获取到授权公众号的appidaccess_tokenrefresh_token,需要结合业务保存到数据库中。

test.php

获取到所需要的信息以后,就可以调用官方的接口来获取信息了:

$app = new Application($config);

# callback.php 中返回的信息
$authorizerAppid = '';
$authorizerAccessToken = '';
$authorizerRefreshToken = '';

// 获取账号信息 https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/authorization-management/getAuthorizerInfo.html
$response = $app->getClient()->post('cgi-bin/component/api_get_authorizer_info', ['json' => ['component_appid'  => $config['app_id'], 'authorizer_appid' => $authorizerAppid]]);
var_dump($response->toArray());

// 获取已发布的文章素材 https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html
$officialAccount = $app->getOfficialAccountWithAccessToken($authorizerAppid, $authorizerRefreshToken);
$response = $officialAccount->getClient()->post('cgi-bin/material/batchget_material', ['json' => ['type' => 'news', 'offset' => 0, 'count' => 20]]);
var_dump($response->toArray());

这样就可以通过$officialAccount来调用公众号的所授权的接口来获取需要的数据了。

🔲 ☆

使用Composer Scripts为依赖进行瘦身

常见的 SDK 都会将所有功能都集成在一个代码库中,本文以腾讯云开发者工具套件(SDK)3.0 为例。

tencentcloud-sdk-php 在 GitHub 的地址是:https://github.com/TencentCloud/tencentcloud-sdk-php

通过执行:

composer require tencentcloud/tencentcloud-sdk-php

来安装完整的 SDK 依赖,而如果只想安装某个产品的,可以使用

composer require tencentcloud/产品名

例如:composer require tencentcloud/cvm

至于具体支持什么产品,可以看到src/TencentCloud目录下有很多的文件夹,包含了对应的产品名,或者直接去 tencentcloud-sdk-php 查找。

我在为 WordPress 的腾讯云对象存储插件增加数据监控的时候用到了云产品监控的 SDK

composer require tencentcloud/monitor

但是只需要用到其中的几个文件,大部分都是无用的,徒增了插件的大小,所以研究了一下将它移除掉,只留下有需要的文件。

composer.json 中增加了如下代码:

    "scripts": {
        "post-install-cmd": "SyncQcloudCos\\Composer::removeUnusedModels",
        "post-update-cmd": "SyncQcloudCos\\Composer::removeUnusedModels"
    },
    "extra": {
        "tencentcloud/monitor": [
            "GetMonitorDataRequest.php",
            "Instance.php",
            "Dimension.php",
            "GetMonitorDataResponse.php",
            "DataPoint.php"
        ]
    }

基于 composer的事件,在执行installupdate时通过removeUnusedModels方法,获取到extra中需要保留的文件,执行删除其他文件的操作。

删除完成后,重新生成一下 classmap,否则classmap里可能还会存在对应的文件信息。

为什么 Composer 在生产环境要使用 dumpautoload?

最后实现代码如下:

namespace SyncQcloudCos;

use Composer\Script\Event;

class Composer
{
    public static function removeUnusedModels(Event $event)
    {
        $composer = $event->getComposer();
        $extra = $composer->getPackage()->getExtra();
        $listedModels = $extra['tencentcloud/monitor'] ?? [];

        if ($listedModels) {
            $vendorPath = $composer->getConfig()->get('vendor-dir');
            $dir = "{$vendorPath}/tencentcloud/monitor/src/TencentCloud/Monitor/V20180724/Models";
            $files = scandir($dir);
            foreach ($files as $file) {
                if ($file === '.' || $file === '..') {
                    continue;
                }
                if (!in_array($file, $listedModels)) {
                    unlink("{$dir}/{$file}");
                }
            }

            $generator = $composer->getAutoloadGenerator();
            $generator->dump($composer->getConfig(), $composer->getRepositoryManager()->getLocalRepository(), $composer->getPackage(), $composer->getInstallationManager(), 'composer', true);
        }
    }
}

Composer Scripts 用途还有很多,对于在 Composer 执行过程中执行包的自定义代码或特定于包的命令非常有用。详细的内容可以查看官方文档

🔲 ⭐

WordPress 支持 S3 的云存储插件

S3 Uploads 是一个打算集成所有支持 S3 的云存储插件。

目前已经支持的厂商有:

  • Cloudflare R2
  • 多吉云
  • 阿里云 OSS
  • 腾讯云 COS
  • 七牛云 KODO
  • 又拍云 USS
  • 其他服务商的可以在评论告诉我...

同时也支持 Typecho 系统。

插件能力

  1. 可配置是否上传缩略图和是否保留本地备份
  2. 本地删除可同步删除腾讯云上面的文件
  3. 支持存储桶绑定的自定义域名
  4. 自带数据库替换功能
  5. 支持同步历史附件到存储桶
  6. 支持上传文件自动重命名
  7. 支持媒体库编辑

插件售价

单读购买 WordPress 或 Typecho 插件售价 ¥ 188
打包购买售价 ¥ 300

购买必读

  • 本插件仅在 qq52o.me 出售,未授权给任何第三方站点;
  • 插件仅允许购买者自己使用;
  • 不得倒卖、转发、共享给他人下载使用;
  • 如有以上行为,不再提供插件更新等售后服务;
  • 购买(支付)成功后,不提供退款/退货服务;

如何购买

支付成功后,请按以下格式用联系邮箱发送邮件到 order[at]qq52o.me

支付宝或微信扫码支付

主题:
购买 S3 Uploads + [订单号](必填)
---
正文:
订单号 :(必填)
支付账号:(必填)
联系邮箱:(必须和发件人一致)
联系QQ:(选填)
网站域名:(必填,仅作记录)
使用系统:(必填,WordPress 或 Typecho)

我确认你的邮件后,会回复给你插件包,并把你的联系邮箱添加到邮件列表,以后插件发布更新会第一时间收到通知。

厂商费用介绍

国内其他厂商的插件和费用见:《沈唁的 WordPress 云存储插件全家桶》

Cloudflare R2

R2 根据存储的数据总量以及对该数据的两类操作进行收费:

  1. A 类操作成本更高并且容易改变状态。
  2. B 类操作倾向于读取现有状态。

出口带宽不收取任何费用。

所有包含的使用量均按月计算。

R2 定价

免费 付费
存储 10 GB/月 每月 $0.015/GB
A 类操作 每月 100 万个请求 百万请求 $4.50
B 类操作 每月 1000 万个请求 百万请求 $0.36
🔲 ☆

怎么复用 GitHub Actions 的 Workflows

WordPress 最近要发布 6.3 版本了,在发布前都会收到WordPress x.x is imminent! Are your plugins ready?的邮件来提醒我更新插件兼容。

以下插件都没及时更新 readme.txt 来表示兼容新版本,主要是觉得挨个更新费时费力。

前几天发现有一个 action(10up/action-wordpress-plugin-deploy) 支持在 GitHub 上直接推送到 WordPress Plugin SVN 中。

所以就写了一个 deploy.yml的工作流:

name: Deploy to WordPress.org
on:
  pull_request:
  release:
    types: [ published ]
jobs:
  tag:
    name: New release
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '7.0'
        tools: composer
    - name: Build
      run: |
        composer install -o --no-dev
      working-directory: cos-sdk-v5
    - name: Set Version
      if: github.event_name == 'pull_request'
      run: |
        echo "VERSION=ci" >> $GITHUB_ENV
    - name: WordPress Plugin Deploy
      id: deploy
      uses: 10up/action-wordpress-plugin-deploy@stable
      with:
        dry-run: ${{ github.event_name == 'pull_request' }}
        generate-zip: ${{ github.event_name == 'release' }}
      env:
        SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
        SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
        SLUG: sync-qcloud-cos
    - name: Upload release asset
      uses: actions/upload-release-asset@v1
      if: github.event_name == 'release'
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ github.event.release.upload_url }}
        asset_path: ${{ steps.deploy.outputs.zip-path }}
        asset_name: ${{ github.event.repository.name }}.zip
        asset_content_type: application/zip

嗯,不错,可以用。但是后来发现问题不是这么简单,现在有 10+插件,挨个复制提交的话,现在是没问题,但是后面如果要修改yml中的内容的话,又得重新挨个修改,依旧是一个重复性的工作。

至此,引出本文要讲述的内容:怎么复用 Workflows?

为什么需要复用?

可以使工作流程可重复使用,而不是从一个工作流程复制并粘贴到另一个工作流程。 自己和有权访问可重用工作流程的任何人都可以从另一个工作流程调用可重用工作流程。

重用工作流程可避免重复。 这使得工作流程更易于维护。

组织也可以构建可集中维护的可重用工作流程库。

创建可重用的工作流程

若要使工作流可重用,on 的值必须包括 workflow_call

on:
  workflow_call:

同时可以定义输入和机密,这些输入和机密可以从调用方工作流程传递,然后在被调用的工作流程中使用。

on:
  workflow_call:
    inputs:
      build:
        default: false
        type: boolean
      php:
        default: "7.0"
        type: string
      working-directory:
        default: sdk
        type: string

如果在调用工作流中使用 secrets: inherit 继承机密,那么即使未在 on 键中显式定义机密,也可以引用它们。

其他的就和普通的工作流程定义一致了。

修改完成后的文件

name: WordPress Plugin Deploy

on:
  workflow_call:
    inputs:
      build:
        default: false
        type: boolean
      php:
        default: "7.0"
        type: string
      working-directory:
        default: sdk
        type: string

jobs:
  deploy:
    name: Deploy to WordPress.org
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup PHP
        if: ${{ inputs.build }}
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ inputs.php }}
          tools: composer
      - name: Install dependencies
        if: ${{ inputs.build }}
        run:
          composer install -o --no-dev
        working-directory: ${{ inputs.working-directory }}

      - name: Set Version
        if: github.event_name == 'pull_request'
        run: |
          echo "VERSION=ci" >> $GITHUB_ENV
      - name: WordPress Plugin Deploy
        id: deploy
        uses: 10up/action-wordpress-plugin-deploy@stable
        with:
          dry-run: ${{ github.event_name == 'pull_request' }}
          generate-zip: ${{ github.event_name == 'release' }}
        env:
          SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
          SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
          SLUG: ${{ secrets.PLUGIN_SLUG }}
      - name: Upload release asset
        uses: actions/upload-release-asset@v1
        if: github.event_name == 'release'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ github.event.release.upload_url }}
          asset_path: ${{ steps.deploy.outputs.zip-path }}
          asset_name: ${{ github.event.repository.name }}.zip
          asset_content_type: application/zip

调用

通过uses语法就可以调用可重用工作流。

可以直接在jobs中调用可重用工作流程,而不是从steps中。

可以使用以下语法之一引用可重用的工作流文件:

  • {owner}/{repo}/.github/workflows/{filename}@{ref} 用于公共和专用存储库中的可重用工作流。
  • ./.github/workflows/{filename} 用于同一存储库中的可重用工作流。

在第一个选项中,{ref} 可以是 SHA、发布标记或分支名称。

name: Deploy to WordPress.org
on:
  pull_request:
  release:
    types: [ published ]
jobs:
  deploy:
    uses: sy-records/.github/.github/workflows/wordpress-plugin-deploy.yaml@main
    with:
      build: true
    secrets: inherit

这样就完成了可复用的工作流程定义。

本文示例中的调用方:https://github.com/sy-records/upyun-uss-wordpress/blob/master/.github/workflows/deploy.yml

本文实例中的被调用方:https://github.com/sy-records/.github/blob/main/.github/workflows/wordpress-plugin-deploy.yaml

完整的内容可以查看 GitHub 文档:Reusing workflows

🔲 ☆

正名示意,我将公众号名改成了真名。

是的,没错,我修改了从 17 年注册的公众号名称,使用真名了。

公众号记录

2023 年 04 月 19 日 “沈唁志”改名“鲁飞”
2023 年 01 月 30 日 通过个人认证审核
2017 年 10 月 09 日 注册“沈唁志”

沈唁的意义

沈唁,这个花名已经记不清从什么时候开始使用的了,反正是个非主流的年代起的。

,读作 yàn,其本义是吊丧,对遭遇丧事表示慰问的意思。后多指对遭受凶事的人的慰劳。

这个花名对我来说是对于自己的过去的一种回忆,希望能在自己的选择下走出来正确的道路,用来激励自己。

我的 GitHub 个人介绍写的是🍃 Don't let your past determine the future. 不要让过去决定未来。

过去不必决定未来的选择、行动或结果。无论过去的错误、失败还是挫折,可以从这些经历中吸取教训,放下消极情绪或自我怀疑,专注于创造一个更好的未来。

而且字使用输入法不太好打出来,有时候可能需要翻好多页,才能找到。所以可能有些人打出不来这个字。

博客的 title 也是沈唁志,19 年的时候做到了百度权重 3,后来没怎么管了以后就降了。这次改名也听别人的建议,顺便把博客的副标题改了,加上了鲁飞,这样在搜索时也能搜到了。

沈唁见得多了,就有人会以为这才是我的真名,实则不是。鲁飞才是。

你们找的是鲁迅,关我周树人什么事?

其他链接

沈唁也会继续使用,在各个技术、社区平台看到的沈唁可能都是我,如果不是,纯属巧合。

包括但不限于以下平台:

🔲 ☆

支持 S3 的 WordPress 和 Typecho 云存储插件

S3 Uploads 是一个打算集成所有支持 S3 的云存储插件。

目前已经支持的厂商有:

  • Cloudflare R2
  • 多吉云
  • 阿里云 OSS
  • 腾讯云 COS
  • 七牛云 KODO
  • 又拍云 USS
  • 百度云 BOS
  • Backblaze B2
  • MinIO
  • 缤纷云
  • 其他服务商的可以在评论告诉我...

同时也支持 Typecho 系统。

插件能力

WordPress

  1. 可配置是否上传缩略图和是否保留本地备份
  2. 本地删除可同步删除对象存储上面的文件
  3. 支持存储桶绑定的自定义域名
  4. 自带数据库替换功能
  5. 支持同步历史附件到存储桶
  6. 支持上传文件自动重命名
  7. 支持媒体库编辑
  8. 支持命令行同步文件

Typecho

  1. 支持附件上传、编辑和删除
  2. 本地删除可同步删除对象存储上面的文件
  3. 支持存储桶绑定的自定义域名
  4. 支持是否保留本地备份

插件售价

单独购买 WordPress 或 Typecho 插件售价 ¥ 188
打包购买售价 ¥ 300

购买必读

  • 本插件已经获得国家认证软件著作权证书
  • 本插件仅在 qq52o.me 出售,未授权给任何第三方站点;
  • 插件仅允许购买者自己使用;
  • 不得倒卖、转发、共享给他人下载使用;
  • 如有以上行为,不再提供插件更新等售后服务;
  • 购买(支付)成功后,不提供退款/退货服务;

设置界面

s3 uploads 设置界面

如何购买

支付成功后,请按以下格式用联系邮箱发送邮件到 order[at]qq52o.me

支付宝或微信扫码支付

主题:
购买 S3 Uploads + [订单号](必填)
---
正文:
订单号 :(必填)
支付账号:(必填)
联系邮箱:(必须和发件人一致)
联系QQ/微信:(选填)
网站域名:(必填,仅作记录)
使用系统:(必填,WordPress 或 Typecho)
PHP版本:(必填)

我确认你的邮件后,会回复给你插件包,并把你的联系邮箱添加到邮件列表,以后插件发布更新会第一时间收到通知。

厂商费用介绍

国内其他厂商的插件和费用见:《沈唁的 WordPress 云存储插件全家桶》

Cloudflare R2

R2 根据存储的数据总量以及对该数据的两类操作进行收费:

  1. A 类操作成本更高并且容易改变状态。
  2. B 类操作倾向于读取现有状态。

出口带宽不收取任何费用。

所有包含的使用量均按月计算。

R2 定价

免费 付费
存储 10 GB/月 每月 $0.015/GB
A 类操作 每月 100 万个请求 百万请求 $4.50
B 类操作 每月 1000 万个请求 百万请求 $0.36
🔲 ☆

PHP中的日期和时间处理函数详解

在 Web 开发中,处理日期和时间是一个常见的任务。PHP 提供了一系列强大的日期和时间处理函数,例如strtotimedateDateTimeImmutable::createFromFormat等。

这些函数使得在不同的时间格式之间进行转换,进行日期和时间计算以及格式化输出变得更加便捷。

本文将深入探讨这三个函数的用法和优势。

1. strtotime 函数

strtotime函数用于将人类可读的日期和时间字符串转换为 Unix 时间戳。它可以接受一个日期时间字符串作为参数,并尝试解析它并将其转换为对应的 Unix 时间戳。除了接受基本的日期时间格式外,它还可以理解各种相对时间表达式。以下是strtotime函数的参数和作用:

strtotime(string $datetime, ?int $baseTimestamp = null): int|false
  • 参数:string $datetime, ?int $baseTimestamp = null
  • $datetime:需要解析的日期时间字符串。
  • $baseTimestamp:可选参数,表示用于计算相对日期的基础时间戳。
  • 返回值:解析成功则返回对应的 Unix 时间戳,解析失败则返回false

使用strtotime函数时,可以传递各种不同格式的日期时间字符串,包括绝对时间(如"2023-08-06"、"15:30:00")以及相对时间(如"tomorrow"、"next week")。

函数会尝试根据传入的字符串进行合理的日期时间转换,方便进行时间的计算和比较。

echo strtotime("2023-08-06 15:30:00"), PHP_EOL;
echo strtotime("tomorrow"), PHP_EOL;
echo strtotime("+1 day"), PHP_EOL;

2. date 函数

date函数用于将 Unix 时间戳格式化为所需的日期和时间字符串。它接受一个格式字符串和一个 Unix 时间戳作为参数,然后返回一个格式化后的日期时间字符串。以下是date函数的参数和作用:

date(string $format, ?int $timestamp = null): string
  • 参数:string $format, ?int $timestamp = null
  • $format:日期时间格式字符串,其中包含各种格式化选项,用于定义输出的日期时间样式。
  • $timestamp:可选参数,表示需要格式化的 Unix 时间戳。默认为time()函数的返回值,即当前的 Unix 时间戳。
  • 返回值:根据指定格式返回格式化后的日期时间字符串。

date函数的第一个参数是日期格式字符串,其中包含各种格式化选项,例如"Y"代表年份,"m"代表月份,"d"代表日期,"H"代表小时,"i"代表分钟,"s"代表秒等。

// set the default timezone to use.
date_default_timezone_set('UTC');

// Prints something like: Monday
echo date("l");

// Prints something like: Monday 8th of August 2005 03:12:46 PM
echo date('l jS \of F Y h:i:s A');

// Prints: July 1, 2000 is on a Saturday
echo "July 1, 2000 is on a " . date("l", mktime(0, 0, 0, 7, 1, 2000));

/* use the constants in the format parameter */
// prints something like: Wed, 25 Sep 2013 15:28:57 -0700
echo date(DATE_RFC2822);

// prints something like: 2000-07-01T00:00:00+00:00
echo date(DATE_ATOM, mktime(0, 0, 0, 7, 1, 2000));

通过组合这些选项,就可以创建出各种不同的日期和时间格式。

3. DateTimeImmutable::createFromFormat 方法

DateTimeImmutable::createFromFormat方法是面向对象的日期和时间处理方式,根据指定的格式将日期字符串解析为DateTimeImmutable对象。

这对于处理不同地区的日期格式或需要更精确解析的日期字符串非常有用。

public static DateTimeImmutable::createFromFormat(string $format, string $datetime, ?DateTimeZone $timezone = null): DateTimeImmutable|false

date_create_immutable_from_format(string $format, string $datetime, ?DateTimeZone $timezone = null): DateTimeImmutable|false

以下是DateTimeImmutable::createFromFormat方法的参数和作用:

  • 参数:string $format, string $datetime, ?DateTimeZone $timezone = null
  • $format:日期时间格式字符串,用于指定输入的日期时间字符串的格式。
  • $datetime:需要解析的日期时间字符串。
  • $timezone:可选参数,用于设置解析后的DateTimeImmutable对象的时区。如果不指定,默认为null,表示使用服务器的时区设置。
  • 返回值:如果解析成功,则返回一个DateTimeImmutable对象,如果解析失败,则返回false

使用DateTimeImmutable::createFromFormat方法时,您需要定义一个格式字符串,该格式字符串与输入的日期时间字符串相匹配。

$dateString = "06/08/2023";
$format = "d/m/Y";
$dateTime = DateTimeImmutable::createFromFormat($format, $dateString);

if ($dateTime instanceof DateTimeImmutable) {
    echo $dateTime->format("Y-m-d"); // 输出:2023-08-06
}

总结

在 PHP 中,日期和时间处理是一个常见但复杂的任务。strtotimedateDateTimeImmutable::createFromFormat这三个函数为我们提供了强大的工具,使得处理不同格式的日期和时间变得更加便捷。

strtotime用于将字符串转换为 Unix 时间戳,date用于将时间戳格式化为可读的字符串,而DateTimeImmutable::createFromFormat则允许更精确地解析日期字符串。

通过熟练掌握这些函数,可以更好地处理和管理日期时间相关的任务,提升 Web 开发效率。

❌