分类: 编程

分享 Go 语言、Docker/K8s、WordPress 开发、小程序实战等编程技术心得。注重代码实战与工程落地,涵盖后端架构、性能优化与开发工具使用技巧。

  • WordPress AI Provider for DeepSeek 插件实现分析

    WordPress AI Provider for DeepSeek 插件实现分析

    前言

    WordPress 6.9 引入了 PHP AI Client SDK,为 WordPress 生态提供了统一的 AI 服务接入标准。本文分析我最近实现的 AI Provider for DeepSeek 插件,它是该 SDK 的 DeepSeek 适配实现,同时支持作为 Composer 包和 WordPress 插件两种使用方式。

    插件架构概览

    插件的目录结构遵循 PSR-4 规范:

    wp-ai-provider-for-deepseek/
    ├── plugin.php                          # WordPress 插件入口
    ├── readme.txt                        # WordPress 插件描述
    ├── assets/images/deepseek.svg        # 供应商 Logo
    └── src/
        ├── autoload.php                 # PSR-4 自动加载器
        ├── Provider/
        │   ├── DeepSeekProvider.php            # 主 Provider 类
        │   └── DeepSeekProviderAvailability.php # API Key 可用性检查器
        ├── Models/
        │   └── DeepSeekTextGenerationModel.php # 文本生成模型实现
        └── Metadata/
            └── DeepSeekModelMetadataDirectory.php # 模型元数据目录
    

    核心实现分析

    1. 插件入口与 Hook 注册(plugin.php)

    插件通过三个关键 Hook 完成初始化:

    // 注册 AI Provider(优先级 5,确保在 AI Client 之后加载)
    add_action('init', __NAMESPACE__ . '\\register_provider', 5);
    
    // 输出 Provider 数据到前端(确保 admin 页面可用)
    add_action('admin_enqueue_scripts', __NAMESPACE__ . '\\ensure_provider_data_output', 5);
    
    // 注册 Connector(WordPress 连接器系统)
    add_action('wp_connectors_init', __NAMESPACE__ . '\\register_connector', 10);
    
    // 自动审批 Connector 使用权限(优先级 20,确保 Approvals_Store 可用)
    add_action('init', __NAMESPACE__ . '\\auto_approve_connector', 20);
    

    设计亮点:

    • register_provider() 先检查 AiClient 类是否存在,避免依赖未安装时 fatal error
    • 使用 $registry->hasProvider() 防止重复注册
    • ensure_provider_data_output() 处理边缘情况:当 AI Client 的脚本未加载时,手动输出 window.aiProviderData

    2. Provider 主类(DeepSeekProvider.php)

    核心类继承自 AbstractApiProvider,实现四个工厂方法:

    class DeepSeekProvider extends AbstractApiProvider
    {
        // API 基础 URL
        protected static function baseUrl(): string
        {
            return 'https://api.deepseek.com';
        }
    
        // 创建模型实例(根据能力分发到具体模型类)
        protected static function createModel(...): ModelInterface
        {
            foreach ($capabilities as $capability) {
                if ($capability->isTextGeneration()) {
                    return new DeepSeekTextGenerationModel(...);
                }
            }
            throw new \RuntimeException('Unsupported model capabilities');
        }
    
        // 创建 Provider 元数据(支持版本化特性)
        protected static function createProviderMetadata(): ProviderMetadata
        {
            $args = [
                'deepseek',
                'DeepSeek',
                ProviderTypeEnum::cloud(),
                'https://platform.deepseek.com/api_keys',
                RequestAuthenticationMethod::apiKey()
            ];
    
            // 1.2.0+ 支持描述
            if (version_compare(AiClient::VERSION, '1.2.0', '>=')) {
                $args[] = __('Text generation with DeepSeek models.', 'ai-provider-for-deepseek');
            }
    
            // 1.3.0+ 支持 Logo
            if (version_compare(AiClient::VERSION, '1.3.0', '>=')) {
                $args[] = dirname(__DIR__, 2) . '/assets/images/deepseek.svg';
            }
    
            return new ProviderMetadata(...$args);
        }
    
        // 自定义可用性检查器(用真实 API 请求验证 Key)
        protected static function createProviderAvailability(): ProviderAvailabilityInterface
        {
            return new DeepSeekProviderAvailability(static::class);
        }
    
        // 模型元数据目录(动态从 API 发现可用模型)
        protected static function createModelMetadataDirectory(): ModelMetadataDirectoryInterface
        {
            return new DeepSeekModelMetadataDirectory();
        }
    }
    

    版本兼容处理是这段代码的精华:通过 version_compare 逐步添加新特性支持,确保插件在旧版 PHP AI Client 上也能正常运行。

    3. API Key 验证与缓存(DeepSeekProviderAvailability.php)

    DeepSeek 不支持标准的”列出模型”端点,因此插件采用真实请求验证法:发送一个 max_tokens=1 的最小化请求,根据 HTTP 状态码判断 Key 是否有效。

    public function isConfigured(): bool
    {
        // 1. 检查缓存(5 分钟过期)
        $cached = get_transient(self::CACHE_KEY);
        if ($cached !== false) {
            return (bool) $cached;
        }
    
        // 2. 获取 API Key(优先 WordPress 选项,fallback 到环境变量)
        $apiKey = $this->getApiKey();
        if (empty($apiKey)) {
            $this->cacheResult(false);
            return false;
        }
    
        // 3. 发送最小化请求
        $response = wp_remote_post($url, [
            'headers' => [
                'Content-Type'  => 'application/json',
                'Authorization' => 'Bearer ' . $apiKey,
            ],
            'body' => json_encode([
                'model'    => 'deepseek-v4-flash',
                'messages' => [['role' => 'user', 'content' => 'test']],
                'max_tokens' => 1,
            ]),
            'timeout' => 30,
        ]);
    
        // 4. 根据状态码判断(200/400 = Key 有效,401/403 = Key 无效)
        $responseCode = wp_remote_retrieve_response_code($response);
        $isConfigured = !in_array($responseCode, [401, 403], true);
    
        // 5. 缓存结果
        $this->cacheResult($isConfigured);
        return $isConfigured;
    }
    

    缓存策略有效减少了重复验证请求:使用 WordPress Transient API,5 分钟过期。注意错误时不缓存,允许下次重试。

    4. 模型元数据动态发现(DeepSeekModelMetadataDirectory.php)

    插件通过调用 DeepSeek 的 /models 端点动态获取可用模型列表,而非硬编码:

    protected function parseResponseToModelMetadataList(Response $response): array
    {
        $responseData = $response->getData();
        
        // 定义 DeepSeek 模型支持的能力和选项
        $textGenerationCapabilities = [
            CapabilityEnum::textGeneration(),
            CapabilityEnum::chatHistory(),
        ];
        
        $textGenerationOptions = [
            new SupportedOption(OptionEnum::systemInstruction()),
            new SupportedOption(OptionEnum::candidateCount()),
            new SupportedOption(OptionEnum::maxTokens()),
            new SupportedOption(OptionEnum::temperature()),
            new SupportedOption(OptionEnum::topP()),
            new SupportedOption(OptionEnum::customOptions()),
            new SupportedOption(OptionEnum::inputModalities()),  // 修复:添加必需的能力声明
            new SupportedOption(OptionEnum::outputModalities()),
        ];
    
        // 将 API 响应映射为 ModelMetadata 对象
        $models = array_map(function ($modelData) use ($textGenerationCapabilities, $textGenerationOptions) {
            return new ModelMetadata(
                $modelData['id'],    // model ID(如 deepseek-v4-flash)
                $modelData['id'],    // model name
                $textGenerationCapabilities,
                $textGenerationOptions
            );
        }, $responseData['data']);
    
        // 排序:deepseek-v4-flash 优先,其次是 deepseek-v4-pro,然后字母序
        usort($models, [$this, 'modelSortCallback']);
        
        return $models;
    }
    

    注意inputModalitiesoutputModalities 的添加——这是修复 ModelRequirements::fromPromptData()generate_text() 调用的关键,缺少这两个选项会导致运行时错误。

    5. 文本生成模型实现(DeepSeekTextGenerationModel.php)

    DeepSeek API 兼容 OpenAI 的 Chat Completions 格式,因此直接继承 AbstractOpenAiCompatibleTextGenerationModel

    class DeepSeekTextGenerationModel extends AbstractOpenAiCompatibleTextGenerationModel
    {
        protected function createRequest(
            HttpMethodEnum $method,
            string $path,
            array $headers = [],
            $data = null
        ): Request {
            // 覆盖父类:使用 DeepSeek Provider 的 URL 而非 OpenAI 的
            return new Request(
                $method,
                DeepSeekProvider::url($path),  // => https://api.deepseek.com/chat/completions
                $headers,
                $data,
                $this->getRequestOptions()
            );
        }
    }
    

    这个实现非常简洁,因为大部分工作(请求格式化、响应解析、流式输出等)都由父类 AbstractOpenAiCompatibleTextGenerationModel 完成。只需要覆盖 createRequest() 将请求路由到 DeepSeek 的 API 地址即可。

    Connector 审批系统

    WordPress 的 AI Client 引入了 Connector 审批机制:每个插件使用 Connector(如 DeepSeek)前必须获得审批。本插件在 init hook(优先级 20)自动审批自己:

    function auto_approve_connector(): void
    {
        if (!class_exists('\WordPress\AI\Connector_Approval\Approvals_Store')) {
            return;  // 旧版 AI Client 不支持审批系统
        }
    
        $store = new \WordPress\AI\Connector_Approval\Approvals_Store();
        $plugin_basename = plugin_basename(__FILE__);
        $connector_id = 'deepseek';
    
        if ($store->is_approved($plugin_basename, $connector_id)) {
            return;  // 已审批,跳过
        }
    
        $store->set_approval($plugin_basename, $connector_id, true);
    }
    

    这避免了用户手动在设置页面审批插件,提升了用户体验。

    使用方式

    作为 WordPress 插件

    // 1. 安装并激活插件(依赖 PHP AI Client 插件)
    // 2. 配置 API Key
    putenv('DEEPSEEK_API_KEY=your-api-key');
    
    // 3. 使用
    $result = AiClient::prompt('解释量子计算')
        ->usingProvider('deepseek')
        ->generateTextResult();
    
    echo $result->toText();
    

    作为 Composer 包

    composer require wordpress/ai-provider-for-deepseek
    
    <?php
    use WordPress\AiClient\AiClient;
    use WordPress\DeepSeekAiProvider\Provider\DeepSeekProvider;
    
    // 注册 Provider
    $registry = AiClient::defaultRegistry();
    $registry->registerProvider(DeepSeekProvider::class);
    
    // 设置 API Key
    putenv('DEEPSEEK_API_KEY=your-api-key');
    
    // 生成文本
    $result = AiClient::prompt('Explain quantum computing')
        ->usingProvider('deepseek')
        ->generateTextResult();
    
    echo $result->toText();
    

    技术亮点总结

    特性 实现方式 价值
    版本兼容 version_compare 渐进式添加特性 支持旧版 AI Client,无 breaking change
    API Key 验证 最小化真实请求 + Transient 缓存 准确验证 + 减少重复请求
    动态模型发现 调用 /models API + 元数据映射 自动支持新模型,无需更新代码
    Connector 审批 init hook 自动审批 零配置用户体验
    OpenAI 兼容 继承 AbstractOpenAiCompatible* 复用大量现有逻辑,代码量极小
    双模式运行 Composer 包 + WordPress 插件 最大灵活性,可在任何 PHP 项目中使用

    结论

    这个插件的实现展示了如何为一个新的 AI 服务商编写符合 WordPress PHP AI Client 标准的 Provider。核心要点:

    1. 继承正确的抽象类:文本生成继承 AbstractOpenAiCompatibleTextGenerationModel
    2. 实现四个工厂方法baseUrl()createModel()createProviderMetadata()createModelMetadataDirectory()
    3. 自定义可用性检查:当标准方法(列出模型)不可用时,用真实请求验证
    4. 版本兼容处理:使用 version_compare 渐进式添加新特性支持
    5. WordPress 集成:正确注册 Hook、Connector、审批

    完整代码已开源在 GitHub,欢迎试用和贡献。

  • Go项目架构实战:从零搭建生产级API服务

    Go项目架构实战:从零搭建生产级API服务

    为什么项目结构很重要

    代码结构是团队协作的隐形成本。好的结构让人一眼看懂模块边界,差的结构让改一个 bug 需要改七个文件。随着项目增长,重新组织结构的代价会越来越高——所以从第一天就把结构做好。

    一种实用的 Go 项目分层

    myapp/
    ├── cmd/
    │   └── server/
    │       └── main.go          # 入口,只负责 wire 和启动
    ├── internal/
    │   ├── handler/             # HTTP/gRPC handler(接收请求)
    │   ├── service/              # 业务逻辑层(纯函数,无依赖注入简化测试)
    │   ├── repo/                 # 数据访问层(DB / Cache / 外部 API)
    │   └── model/                # 数据模型(贫血模型,纯数据结构)
    ├── pkg/
    │   └── response/             # 对外暴露的公共库
    ├── migrations/              # 数据库迁移文件
    ├── config/
    │   └── config.go             # 配置加载
    ├── Makefile
    └── go.mod

    各层职责定义

    Handler 层负责解析请求参数、做基础校验、调用 Service 层、组装响应。注意:Handler 不写任何业务逻辑。

    Service 层包含核心业务逻辑。推荐用纯函数风格,依赖通过参数传入,方便写单元测试。

    Repo 层封装所有数据访问,只对 Service 层暴露存储抽象——换数据库、换缓存策略,不影响上层代码。

    避免几个常见反模式

    • 把 model 当贫血模型塞进 repo 层,导致 repo 越来越胖
    • 在 handler 里直接写 SQL,耦合数据库细节
    • 全局变量(var db *sql.DB)跨包共享,测试时无法 mock
    • 把所有类型都放在 model 包,没有按领域聚合

    小结

    Go 的项目结构没有标准答案,但有一个原则:让每一层都专注于一件事。如果你的 handler 里有 SQL,你的 service 里在 new 各种客户端,那是结构在提醒你需要调整了。

    来源:https://www.dnote.cn

  • Go 并发编程:Goroutine 与 Channel 实战指南

    Go 并发编程:Goroutine 与 Channel 实战指南

    为什么 Go 的并发模型值得专门学

    Go 的并发哲学来自 C.A.R. Hoare 的 CSP 理论,与传统线程模型完全不同。理解这套模型,是写出高性能 Go 服务的基础。

    Goroutine 到底是什么

    goroutine 是由 Go 运行时管理的轻量级线程,创建成本极低(约 2KB 栈空间),可以轻松创建数万个 goroutine 而不耗尽系统资源。与 OS 线程的 1:1 模型不同,Go 使用 M:N 调度——M 个 OS 线程上运行 N 个 goroutine,由 Go 运行时 GOMAXPROCS 控制并行度。

    一个简单的 goroutine 示例

    // 启动一个 goroutine
    go func() {
        fmt.Println("running in goroutine")
    }()
    
    // 主 goroutine 继续执行,不等待子 goroutine
    fmt.Println("main exits")

    Channel:goroutine 之间的通信桥梁

    channel 是 goroutine 间传递数据的管道,有带缓冲(buffered)和不带缓冲(unbuffered)两种。推荐优先使用无缓冲 channel,这样发送与接收必须配对,强制调用方思考同步问题,避免隐藏的竞态。

    ch := make(chan int)         // 无缓冲
    ch <- 42                       // 发送(阻塞直到有人接收)
    value := <-ch                  // 接收
    
    // 带缓冲 channel(队列)
    bufCh := make(chan int, 100)  // 最多缓存 100 条,不阻塞

    select:多路复用的并发协调

    select 关键字让一个 goroutine 可以同时监听多个 channel,就跟 select 系统调用监听多个 fd 一样。配合 default case 可以实现非阻塞发送/接收。

    select {
    case msg := <-ch1:
        fmt.Println("received from ch1:", msg)
    case msg := <-ch2:
        fmt.Println("received from ch2:", msg)
    case <-time.After(time.Second):
        fmt.Println("timeout")
    default:
        fmt.Println("no message available")
    }

    常见踩坑与最佳实践

    • 不要用 channel 传递所有数据——共享内存(sync.Mutex/sync.Map)有时更简单
    • 永远不要关闭由别人发送的 channel,避免 panic
    • 使用 context.Context 来控制 goroutine 的生命周期,而不是用 channel 硬传退出信号
    • 在生产环境加上 -race 参数跑单元测试,检查数据竞争

    总结

    Go 的并发模型用起来简单,但要真正用好,需要理解 goroutine 的调度原理、channel 的阻塞语义,以及 context 在多层调用中的传递方式。掌握这些,你就能写出既高效又安全的并发代码。

    来源:https://www.dnote.cn

  • Go部署:Docker/K8s实战配置

    Go部署:Docker/K8s实战配置

    多阶段构建:让镜像小到 10MB 以内

    Go 是静态编译语言,只需要编译后的二进制文件,不需要运行时环境。多阶段构建可以分离构建环境和运行环境,最终镜像只包含二进制文件。

    # 阶段1:构建
    FROM golang:1.23-alpine AS builder
    WORKDIR /app
    COPY go.mod go.sum ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -s' -o server .
    
    # 阶段2:运行
    FROM alpine:3.19
    RUN addgroup -S appgroup && adduser -S appuser -G appgroup
    WORKDIR /app
    COPY --from=builder /app/server .
    RUN chmod +x server
    USER appuser
    CMD ["./server"]

    利用 Docker BuildKit 加速构建

    # 启用 BuildKit
    export DOCKER_BUILDKIT=1
    
    # 并行下载依赖,大幅加速
    docker build --progress=plain -t myapp:latest .

    健康检查:让 K8s/Compose 知道服务已就绪

    在 Dockerfile 里加上 HEALTHCHECK,让 orchestrator 在服务真正能接收请求时才将其加入负载均衡。

    HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
      CMD wget -qO- http://localhost:8080/health || exit 1

    环境变量注入的坑

    Go 程序默认会在 ENV 改变时重新加载,但 Docker 层会缓存 ENV。用 ENTRYPOINT 脚本或 Viper 动态读取,比硬编码 ENV 常量更灵活。

    小结

    Docker 化 Go 服务的核心是:镜像要小(多阶段构建)、构建要快(BuildKit)、健康要可感知。做到这三点,生产部署就没什么大问题了。

    来源:https://www.dnote.cn

  • Go ORM对比:GORM vs Ent vs Kitex

    Go ORM对比:GORM vs Ent vs Kitex

    为什么 Go 生态里有这么多 ORM 选择

    Go 生态偏好显式而非隐式,所以没有 Java Hibernate 那样的大一统 ORM。GORM、Ent 和 Kitex(配合 codegen)是三条不同的路,各有适用场景。

    GORM:最成熟,上手最快

    GORM 是事实标准,生态丰富、文档完善。缺点是隐式 SQL、链式 API 容易写出难以维护的代码,且性能一般。适合快速开发或中小型项目。

    Ent:代码生成,类型安全

    Ent 是 Facebook 出品的代码生成式 ORM。Schema 定义后自动生成代码,类型安全、IDE 友好、支持图结构查询。缺点是学习曲线稍陡,新增字段需要重新生成。

    Kitex + SQLBricks:微服务场景的最佳组合

    字节跳动的 Kitex 框架不绑定 ORM,推荐配合 sqlx 或 sqlc 使用。sqlc 可以从 SQL 语句生成类型安全的 Go 代码,彻底避免 SQL 注入,同时保持 SQL 的可读性。

    各场景推荐

    场景 推荐 原因
    快速原型/MVP GORM 上手快,生态成熟
    中大型项目 Ent 类型安全,可维护
    高性能微服务 Kitex + sqlx 零抽象,性能可控
    需要图查询 Ent 原生支持图结构

    小结

    选 ORM 就是选抽象程度:GORM 是高抽象(牺牲性能和可维护性),Ent 是中抽象(平衡),sqlx 是低抽象(保留 SQL 的所有优点)。没有最优解,只有最适合当前项目规模和团队风格的方案。

    来源:https://www.dnote.cn

  • Go并发编程精讲:goroutine/channel原理与避坑指南

    Go并发编程精讲:goroutine/channel原理与避坑指南

    深入理解 GMP 调度模型

    Go 运行时调度器将 goroutine 映射到 OS 线程上执行,GMP 代表 G(Goroutine)、M(Machine/OS 线程)、P(Processor,执行上下文)。P 的数量默认等于 CPU 核心数,可以通过 GOMAXPROCS 调整,这是 Go 并发性能的关键参数。

    goroutine 的生命周期与状态

    goroutine 有多个状态:waiting(阻塞在 channel 或系统调用)、runnable(就绪等待调度)、running(正在执行)。理解这些状态有助于分析死锁和性能问题。

    channel 的底层实现

    channel 内部有环形队列和两个等待队列(sendq 和 recvq)。向已满的无缓冲 channel 发送会阻塞,goroutine 被挂在 sendq 上;接收时会从 recvq 唤醒一个 sender 或从队列取数据。

    常见并发陷阱

    • 向已关闭的 channel 发送数据会 panic,但接收会返回零值
    • 多个 goroutine 同时等待同一个 channel,只有 1 个会被唤醒——用 sync.WaitGroup 而非 channel 控制退出
    • for range channel 会一直阻塞直到 channel 关闭
    • context.WithCancel 是优雅退出多个 goroutine 的标准方式

    小结

    理解 GMP 调度和 channel 底层原理,才能真正写出高效的并发代码。遇到性能问题时,用 runtime.NumGoroutine() 和 pprof 分析 goroutine 的状态分布是第一步。

    来源:https://www.dnote.cn

  • 用 Taro 开发小程序:答题卡网格布局的兼容性问题总结

    用 Taro 开发小程序:答题卡网格布局的兼容性问题总结

    背景

    在做一个刷题小程序时,需要实现一个答题卡界面——用网格展示所有题目状态(未答/已答/当前题),支持点击跳转。在 Web 上几行 CSS 搞定,在小程序里却踩了不少坑。

    问题一:gap 属性在小程序里不生效

    这是最常见的问题。CSS gap 属性用于设置网格/弹性盒子的间距,在主流浏览器都支持,但在微信小程序、支付宝小程序里均不支持。解决方案:

    /* ❌ 小程序不支持 */
    .grid { display: grid; gap: 16px; }
    
    /* ✅ 改用 margin 模拟 */
    .grid { display: grid; }
    .grid-item { margin-right: 16px; margin-bottom: 16px; }
    /* 最后一项去掉右边距 */
    .grid-item:last-child { margin-right: 0; }

    问题二:display: grid 在某些机型的兼容情况

    iOS 微信大部分版本支持 grid,但 Android 端部分机型(尤其是低端机)的 UC/QQ 浏览器内核不支持。推荐降级方案:用 flex + 百分比宽度实现网格。

    /* ✅ 兼容写法:flex 模拟网格 */
    .row { display: flex; flex-wrap: wrap; }
    .cell { width: 48px; height: 48px; margin: 0 4px 8px 0; }

    问题三::nth-child 在某些场景下失效

    nth-child 在动态列表(数据来自接口)时表现正常,但在用 v-for 遍历固定数量的数字时,微信开发者工具和部分真机行为不一致。解决方案是改用 v-if 或加辅助 class。

    实战答题卡组件代码

    const AnswerCard = ({ questions, currentIndex, onJump }) => {
      return (
        <View className='answer-card'>
          <View className='row'>
            {questions.map((q, i) => (
              <View
                key={q.id}
                className={`cell ${i === currentIndex ? 'current' : ''} ${q.answered ? 'done' : 'undone'}`}
                onClick={() => onJump(i)}
              >
                {i + 1}
              </View>
            ))}
          </View>
        </View>
      )
    }

    小结

    小程序开发的本质是与 CSS 兼容层打交道。遇到 display、gap、nth-child 等属性,先查 caniuse,再想降级方案。开发阶段多用真机调试,开发者工具的结果不一定准确。

    来源:https://www.dnote.cn

  • Docker 容器化 Go 服务的几个实战细节

    Docker 容器化 Go 服务的几个实战细节

    多阶段构建:让镜像小到 10MB 以内

    Go 是静态编译语言,只需要编译后的二进制文件,不需要运行时环境。多阶段构建可以分离构建环境和运行环境,最终镜像只包含二进制文件。

    # 阶段1:构建
    FROM golang:1.23-alpine AS builder
    WORKDIR /app
    COPY go.mod go.sum ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -s' -o server .
    
    # 阶段2:运行
    FROM alpine:3.19
    RUN addgroup -S appgroup && adduser -S appuser -G appgroup
    WORKDIR /app
    COPY --from=builder /app/server .
    RUN chmod +x server
    USER appuser
    CMD ["./server"]

    利用 Docker BuildKit 加速构建

    # 启用 BuildKit
    export DOCKER_BUILDKIT=1
    
    # 并行下载依赖,大幅加速
    docker build --progress=plain -t myapp:latest .

    健康检查:让 K8s/Compose 知道服务已就绪

    在 Dockerfile 里加上 HEALTHCHECK,让 orchestrator 在服务真正能接收请求时才将其加入负载均衡。

    HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
      CMD wget -qO- http://localhost:8080/health || exit 1

    环境变量注入的坑

    Go 程序默认会在 ENV 改变时重新加载,但 Docker 层会缓存 ENV。用 ENTRYPOINT 脚本或 Viper 动态读取,比硬编码 ENV 常量更灵活。

    小结

    Docker 化 Go 服务的核心是:镜像要小(多阶段构建)、构建要快(BuildKit)、健康要可感知。做到这三点,生产部署就没什么大问题了。

    来源:https://www.dnote.cn

  • WordPress 6.9.4 正式发布:更新内容速览

    WordPress 6.9.4 已于 2026年04月15日 正式发布。以下是本次更新的主要内容整理,供站长参考。

    ⚠️ 安全版本:本次更新包含安全修复,建议所有用户尽快升级。

    本次 6.9.4 是一个安全维护版本,修复了 10 个安全问题。WordPress 安全团队发现此前版本中部分安全修复未完全生效,因此紧急发布 6.9.4 补丁。

    🔒 安全修复

    • 路径穿越漏洞(PclZip 组件)
    • 授权绕过漏洞
    • XXE(XML 外部实体注入)漏洞

    如何升级

    登录 WordPress 后台 → 仪表盘 → 更新,点击「立即更新」即可完成升级。建议升级前先备份数据库和文件。

    参考链接

    本文由自动化脚本监测到 WordPress 新版本后自动生成,仅供参考。

  • Dnote网站深度诊断报告:发现的问题与优化建议

    Dnote网站深度诊断报告:发现的问题与优化建议

    前言

    最近对个人博客网站 dnote.cn 进行了一次全面的结构分析和诊断,发现了一些值得关注的问题。本文将详细记录发现的问题并提供相应的优化建议。

    一、网站基础数据

    • 文章总数:100篇
    • 分类数:3个(编程95篇、投资18篇、未分类3篇)
    • 标签数:22个
    • 页面数:3个(关于、小玩意儿、搜索)
    • 媒体文件:50个

    二、发现的问题

    1. 内容分类严重失衡

    在100篇文章中,编程类占据了95篇,比例高达95%,而投资类仅有18篇。这反映出内容定位过于单一,建议:

    • 将部分技术文章重新归类,细化分类标签
    • 考虑增加新的分类维度,如”产品”、”创业”、”生活”
    • 将”未分类”的3篇文章尽快归类

    2. SEO基础优化不足

    通过分析发现以下SEO问题:

    • 缺少结构化数据:未发现JSON-LD富媒体标记,错失搜索展示机会
    • 内链锚文本单一:多为”查看更多→”,应使用关键词锚文本
    • 文章摘要较短:部分文章描述信息不足,影响搜索结果吸引力

    3. 性能优化空间

    • 未使用CDN:静态资源未通过CDN分发
    • 缺少Gzip压缩:建议开启以减少传输体积
    • 缺少浏览器缓存配置:应设置合理的缓存策略

    4. 标签使用混乱

    22个标签中,部分标签存在重复或过细的问题,如同时存在”SQL”和”MySQL”、”Docker”和”Linux”等,建议进行标签整合。

    5. 文章日期异常

    所有文章显示发布日期为2026年3月13日(部分甚至显示2025年),这可能是:

    • 批量导入时的伪日期
    • 系统时间配置问题
    • WordPress时区设置问题

    建议核查并修正,避免搜索引擎误判内容为过期内容。

    三、优化建议汇总

    优先级 问题 建议方案
    分类失衡 重新划分分类,增加3-5个新分类
    日期异常 检查WordPress时区设置,修正文章日期
    SEO元标签 添加JSON-LD结构化数据
    内链优化 使用关键词作为锚文本
    性能优化 接入CDN,配置Gzip和缓存

    四、结语

    整体而言,网站技术架构合理,WordPress运行稳定,内容质量较高。主要问题集中在内容组织结构和SEO基础优化上。解决这些问题后,网站的搜索可见性和用户体验都会有明显提升。

    后续我会逐步进行优化并记录改进效果,敬请期待。

  • 在electron中基于容器和服务提供者扩展应用核心能力

    在electron中基于容器和服务提供者扩展应用核心能力

    应用自身可能提供多种不同的能力,结合服务提供者概念和容器,我们可以实现类似插件的扩展机制,并且通过容器来统一管理服务对象,方便后续扩展。

    一、容器

    容器提供注册同步/异步工厂的方法、同步/异步获取指定服务对象的方法,移除某服务的方法和清空全部服务对象的方法。

    export class Container {
      private instances: Map<string, any> = new Map()
      private factories: Map<string, () => any> = new Map()
      private asyncFactories: Map<string, () => Promise<any>> = new Map()
      private resolvingPromises: Map<string, Promise<any>> = new Map()
      private singleton: Set<string> = new Set()
    
      // 同步注册工厂(默认为单例)
      register<T>(token: string, factory: () => T, options: { singleton?: boolean } = { singleton: true }) {
        this.factories.set(token, factory)
        if (options.singleton) {
          this.singleton.add(token)
        } else {
          this.singleton.delete(token)
        }
      }
    
      // 异步注册工厂(默认为单例)
      registerAsync<T>(token: string, factory: () => Promise<T>, options: { singleton?: boolean } = { singleton: true }) {
        this.asyncFactories.set(token, factory)
        if (options.singleton) {
          this.singleton.add(token)
        } else {
          this.singleton.delete(token)
        }
      }
    
      // 同步解析
      resolve<T>(token: string): T {
        // 如果是单例且已存在实例,直接返回
        if (this.singleton.has(token) && this.instances.has(token)) {
          return this.instances.get(token)
        }
    
        // 查找工厂函数
        const factory = this.factories.get(token)
        if (!factory) {
          // 如果没有工厂但有实例,可能是异步实例已完成
          if (this.instances.has(token)) {
            return this.instances.get(token)
          }
          throw new Error(`Service not found: ${token}`)
        }
    
        // 创建实例
        const instance = factory()
    
        // 如果是单例,保存实例
        if (this.singleton.has(token)) {
          this.instances.set(token, instance)
        }
    
        return instance
      }
    
      // 异步解析
      async resolveAsync<T>(token: string): Promise<T> {
        // 如果是单例且已存在实例,直接返回
        if (this.singleton.has(token) && this.instances.has(token)) {
          return this.instances.get(token)
        }
    
        // 检查是否已经有正在解析的Promise
        let resolvingPromise = this.resolvingPromises.get(token)
        if (resolvingPromise) {
          // 如果有,直接返回这个Promise的结果
          return resolvingPromise
        }
    
        // 先检查是否有异步工厂
        const asyncFactory = this.asyncFactories.get(token)
        if (asyncFactory) {
          // 创建一个新的解析Promise并存储
          resolvingPromise = (async () => {
            try {
              const resolved = await asyncFactory()
              // 如果是单例,保存实例
              if (this.singleton.has(token)) {
                this.instances.set(token, resolved)
              }
              return resolved
            } finally {
              // 解析完成后从resolvingPromises中移除
              this.resolvingPromises.delete(token)
            }
          })()
    
          // 如果是单例,存储Promise以避免重复创建
          if (this.singleton.has(token)) {
            this.resolvingPromises.set(token, resolvingPromise)
          }
          return resolvingPromise
        }
    
        // 再检查是否有同步工厂
        const factory = this.factories.get(token)
        if (factory) {
          try {
            const resolved = factory()
            // 如果是单例,保存实例
            if (this.singleton.has(token)) {
              this.instances.set(token, resolved)
            }
            return resolved
          } catch (error) {
            if (error instanceof Error) {
                throw new Error(`Failed to resolve service '${token}': ${error.message}`)
            } else {
                throw error
            }
          }
        }
    
        // 最后检查是否已有实例(可能是之前异步解析完成的)
        if (this.instances.has(token)) {
          return this.instances.get(token)
        }
    
        throw new Error(`Service not found: ${token}`)
      }
    
      // 检查是否存在指定token的服务
      has(token: string): boolean {
        return this.instances.has(token) ||
               this.factories.has(token) ||
               this.asyncFactories.has(token)
      }
    
      // 清除指定服务的实例(不移除工厂)
      clearInstance(token: string): void {
        this.instances.delete(token)
        this.resolvingPromises.delete(token)
      }
    
      // 移除服务(包括工厂和实例)
      remove(token: string): void {
        this.instances.delete(token)
        this.factories.delete(token)
        this.asyncFactories.delete(token)
        this.resolvingPromises.delete(token)
        this.singleton.delete(token)
      }
    }
    

    二、服务提供者

    服务提供者接口比较简单,区分register和boot主要是为了确保boot中可以调用任何其他提供者。

    import type { Container } from "../container"
    
    export default interface ServiceProvider {
        /**
         * 应用启动后立即调用
         * 建议仅用于注册服务,不要执行其他操作,其他操作可以放在boot方法中
         *
         * @param container
         */
        register?(container: Container): void
    
        /**
         * boot函数在所有服务注册完成后调用
         * 可以执行一些初始化操作
         *
         * @param container
         */
        boot?(container: Container): void | Promise<void>
    }
    

    三、应用对象

    import { Container } from './container'
    
    export default class APP {
    
        /**
         * 应用实例,用来保持单例
         */
        private static _ins: APP
    
        /**
         * 容器对象
         */
        private container: Container
    
        /**
         * 服务提供者列表
         */
        private providers: ServiceProvider[] = []
    
        /**
         * 运行应用
         */
        static run() {
            if (APP._ins == null) {
                APP._ins = new APP()
            }
    
            return APP._ins
        }
    
        constructor() {
            // 初始化容器并注册服务提供者
            this.container = new Container()
    
            // 注册服务
            this.registerProviders()
    
            // 启动服务
            this.bootProviders()
        }
    
        registerProviders() {
            for (const provider of this.providers) {
                provider.register?.(this.container)
            }
        }
    
        bootProviders() {
            return Promise.all(
                this.providers.map(provider => provider.boot?.(this.container))
            )
        }
    }
    
  • Caddy配置sts(Strict-Transport-Security)

    Caddy配置sts(Strict-Transport-Security)

    example.com {
        # 启用 HSTS,有效期 1 年(31536000 秒)
        header {
            Strict-Transport-Security max-age=31536000
        }
    
        # 其他配置...
        reverse_proxy localhost:8080
    }
    
  • 创建strapi插件

    创建strapi插件

    需要开发一个插件来实现一些自定义功能,官方文档写的实在是难以理解了。

    1. 创建基础的目录结构

    # 作为项目根目录
    mkdir xxx
    
    # 进入项目目录
    cd xxx
    
    # 创建strapi 项目
    npx create-strapi@latest strapi
    
    # 创建插件
    npx @strapi/sdk-plugin init strapi-plugin
    

    2. 配置工作区

     # xxx目录中添加workspace配置
    {
         "workspaces": [
             "strapi",
             "strapi-plugin"
         ],
         "dependencies": [
             "strapi-plugin": "workspace:*"
         ]
     }
    

    3. strapi目录引入依赖

     {
         ...
         "dependencies": {
             ...
             "strapi-plugin": "0.0.0"
         }
     }
    

    4. 插件目录监听文件修改

    # 根目录执行
    npm run watch --workspace=strapi-plugin
    

    5. 运行strapi

    npm run develop --workspace=strapi
    
  • Parse Platform任务调度

    Parse Platform任务调度

    说起来也挺奇怪的,Parse Platform本身基于js实现,Node生态里的任务调度实现起来并不麻烦,Parse却选择了不集成,使用第三方提供的任务调度。这里使用node-cron来实现Parse Server中的Job调度。

    一、Cloud Code定义Job

    Parse.Cloud.job("test", async (request) =>  {
        // params: passed in the job call
        // headers: from the request that triggered the job
        // log: the ParseServer logger passed in the request
        // message: a function to update the status message of the job object
        const { message } = request
        message('done')
    })
    

    二、定义一个调用Job的函数,方便后面使用

    async function callParseJob(parseServerUrl: string, name: string, data?: any) {
      console.log(`node-cron: callParseJob ${name}`)
      const options: RequestInit = {
        method: 'POST',
        headers: {
          'X-Parse-Application-Id': process.env.APP_ID ?? '',
          'X-Parse-Master-Key': process.env.MASTER_KEY ?? ''
        }
      }
      if (data != null) {
        options.body = JSON.stringify(data)
      }
      const res = await fetch(`${parseServerUrl}/jobs/${name}`, options)
      console.log(`node-cron: job ${name} finished, ${res.statusText}`)
      return await res.json()
    }
    

    三、使用node-cron调度Job

    这里选择通过单独的进程来运行调度程序

    if (cluster.isPrimary) {
      cron.schedule('* * * * *', async () => await callParseJob(process.env.SERVER_URL ?? '', 'test'))
      cluster.fork() // 根据cpu个数fork子进程
    
      cluster.on('exit', function(worker, code, signal) {
        console.log('worker ' + worker.process.pid + ' exited')
        cluster.fork() //新建一个worker
      })
    } else {
      const app = express()
        //...启动Server相关的代码,这里省略
    }
    
  • 为什么mongoDB在国内发展的没redis好?

    为什么mongoDB在国内发展的没redis好?

    mongo用户应该集中在前端转全栈,通过Node操作mongodb,可以保持之前对于JSON的理解,不用接触传统数据库的知识。

    国内用Node作为后端的用户就不多,Serverless发展也一般,搞Node全栈的集中在独立开发者或者大型公司的某个团队,这个体量确实也不是很大。

    如果是纯后端,我很难理解引入Mongo的意义,完全可以把Postgres的表搞成id+json的形式来模拟Mongo无架构,甚至微软搞了一个DocumentDB,兼容mongo协议,就是基于PostgreSQL的。从扩展性,功能,性能任何方面看,这样用pg都是优于采用MongoDB的。

    DocumentDB:开源公告 – Microsoft 开源博客

    Redis不一样,

    1. 大部分应用考虑扩展和性能的情况下,都需要一个缓存中间件,这个首先mongo就不适合
    2. 简单消息队列
    3. 发布订阅
    4. 不提供内置有序集合的语言,有序集合也省的自己写了
    5. 高频读写的小数据也可以开持久化作为主数据库

     

    https://www.zhihu.com/question/478485839/answer/1965357311661965488

  • 阿里云OSS与Cloudflare R2价格对比

    阿里云OSS与Cloudflare R2价格对比

    场景:存储位置美西硅谷,存储量200GB,时间一年,写操作3万/月,读操作30万/月,每月传输200GB流量。

    1. 阿里云

    1. 存储费用:100GB资源包+100GB按量 = 162 + 0.136 元/GB/月 * 100 * 12 = 162 + 163.2 = 325.2(每月免费5GB,单价合¥0.136 元/GB/月
    2. 写操作:0(地域500万次/月免费,超出0.1/万次)
    3. 读操作:0(地域2000万次/月免费,超出0.1/万次)
    4. 流量费用:200 * 0.5 * 12 = 1200
    5. 合计:1525.2

    2. Cloudflare

    1. 存储费用:(200 – 10) * 0.015 * 12 = $34.2(每月免费10GB,合¥0.109/GB/月)
    2. 写操作:0(地域100万次/月免费,超出$4.5/百万次,合0.33/万次)
    3. 读操作:0(地域1000万次/月免费,超出0.36/百万次, 合0.026/万次)
    4. 流量费用:0
    5. 合计折合人民币7.13 * 34.2 = 243.846

    3. 腾讯云

    1. 存储费用:212.16(0.089 元/GB/月
    2. 读写操作:0.01/月/万次 * (30 + 3)万次 * 12 = 3.96
    3. 流量费用:200GB/月资源包 = 1029
    4. 合计: 1245.12

    境外走Cloudflare的CDN的话,三方都没有流量费,伴随使用量增加,估计最终Cloudflare整体的价格相对会更低一些,但是需要增加到一定的用量以后。

  • 优化WordPress后台加载速度

    优化WordPress后台加载速度

    1. PHP预加载脚本

    <?php
    
    /**
    *使用OPcache优化WordPress的预加载脚本。
    *将此文件放在WordPress安装的根目录中。
     */
    
    // Define the base path for WordPress
    define('WP_ROOT_DIR', __DIR__);
    
    // Define ABSPATH (required by WordPress core files)
    if ( ! defined( 'ABSPATH' ) ) {
        define( 'ABSPATH', WP_ROOT_DIR . '/' );
    }
    
    // Define WPINC (required by wp-includes/functions.php)
    if ( ! defined( 'WPINC' ) ) {
        define( 'WPINC', 'wp-includes' );
    }
    
    // Define WP_DEBUG (required by wp-includes/functions.php)
    if ( ! defined( 'WP_DEBUG' ) ) {
        define( 'WP_DEBUG', false );
    }
    
    // 仅预加载最基本的核心文件
    require WP_ROOT_DIR . '/wp-includes/default-constants.php';
    require WP_ROOT_DIR . '/wp-includes/rewrite.php';
    require WP_ROOT_DIR . '/wp-includes/theme.php';
    require WP_ROOT_DIR . '/wp-includes/post.php';
    require WP_ROOT_DIR . '/wp-includes/meta.php';
    require WP_ROOT_DIR . '/wp-includes/user.php';
    require WP_ROOT_DIR . '/wp-includes/cache.php';
    require WP_ROOT_DIR . '/wp-includes/capabilities.php';
    require WP_ROOT_DIR . '/wp-includes/shortcodes.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-query.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-widget.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-roles.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-user.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-post.php';
    
    // 后台管理模块的类
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-media-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-users-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-themes-list-table.php';
    
    // Preload database-related files
    require WP_ROOT_DIR . '/wp-includes/wp-db.php';
    // require WP_ROOT_DIR . '/wp-includes/class-wpdb.php'; // 如果不需要,可以注释
    
  • Go语言获取指定年份生肖

    Go语言获取指定年份生肖

    根据给定年份,返回生肖字符串,公元前使用负值即可。(比如2022年,调用使用GetShengXiao(2022),公元前21年,调用使用GetShengXiao(-21))。
    
    
    // 获取生肖索引
    func GetShengXiaoIndex(year int) int {
        // 不存在0年
        if year == 0 {
            panic("err: invalid year")
        }
    
        // 公元前补1
        if year < 0 {
            year += 1
        }
    
        idx := (year - 4) % 12
    
        if idx < 0 {
            idx += 12
        }
    
        return idx
    }
    
    // 根据给定年份获取生肖
    func GetShengXiao(idx int) string {
        return [12]string{"鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"}[idx]
    }
    
  • WordPress添加关键词和描述标签

    WordPress添加关键词和描述标签

    关键词和描述标签作为SEO的基础配置,在wordpress中没有默认添加,这里记录一下在Wordpress中自动添加关键词和描述标签的方法。

    一、实现代码

    实现思路是使用标签来作为关键词,使用文章摘要作为页面描述,找到使用主题的functions.php文件,添加以下代码即可实现。

    /**
     * 添加SEO相关的Keywords和Description标签
     */
    function add_seo_meta_tags() {
        if (is_home()) {
            $tags = get_tags([
                'number' => 15,
                'orderby' => 'count',
                'order' => 'DESC'
            ]);
            $tags = array_map(function ($item) {
                return $item->name;
            }, $tags);
            if (count($tags) > 0) {
                ?>
                <meta name="keywords" content="<?php echo get_bloginfo('name'); ?>,<?php echo implode(',', $tags); ?>">
                <?php
            }
            ?>
                <meta name="description" content="<?php echo get_bloginfo('description'); ?>">
            <?php
        } else if (is_category() || is_tag()) {
            ?>
                <meta name="keywords" content="<?php echo single_cat_title(); ?>">
                <meta name="description" content="<?php echo strip_tags(category_description()); ?>">
            <?php
        } else if (is_singular()) {
            ?>
                <meta name="description" content="<?php echo strip_tags(get_the_excerpt()); ?>">
            <?php
    
            $tags = array_map(function ($item) {
                return $item->name;
            }, get_the_tags() ?: []);
            if (count($tags) > 0) {
                ?>
                    <meta name="keywords" content="<?php echo implode(',', $tags); ?>">
                <?php
            }
        }
    }
    add_action( 'wp_head', 'add_seo_meta_tags' );
    

    二、使用的前置要求

    请注意,需要保证主题的header.php调用过wp_head()函数,类似下面:

    <!DOCTYPE html>
    <html <?php language_attributes(); ?> class="no-js">
    <head>
    <meta charset="<?php bloginfo( 'charset' ); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
    <?php wp_head(); ?>
    </head>
    

    OK,这样我们就给所有页面都添加上关键词和描述标签了~

    相关链接:

    1. wordpress关于meta标签的说明
  • 使用SSH转发服务器端口到本地

    使用SSH转发服务器端口到本地

    注意远程地址、远程端口号、本地地址、本地端口号需要按照实际情况修改。

    function forwardPort() {
        PROC_NAME="ssh -fR $1"
        ProcNumber=`ps -ef |grep -w "$PROC_NAME"|grep -v grep|wc -l`
        if [ $ProcNumber -le 0 ];then
            echo "$1 is not forward.."
            ssh -fCNR $1:localhost:$1 root@0.0.0.0 -p 1234 -o ServerAliveInterval=30
        else
            echo "$1 has forwarded.."
        fi
    }
    for port in 1234 4567
    do
        forwardPort $port
    done
    

    注意,如果需要外网访问转发的端口,需要在远程服务器的配置文件(/etc/ssh/sshd_config)中添加:

    AllowTcpForwarding yes